From 7583ba87292e085fcb4f28b5306f3a379f3ab70c Mon Sep 17 00:00:00 2001 From: bryanthaboi Date: Tue, 18 Aug 2026 14:19:42 -0400 Subject: [PATCH 1/8] CLOSES #1471 --- .github/ISSUE_TEMPLATE/feature_request.yml | 118 ------------------- .github/ISSUE_TEMPLATE/mod_request.yml | 130 --------------------- README.md | 8 +- docs/tiled-map-editing.md | 65 +++++++++++ src/core/ChipAudio.lua | 50 +++++++- src/core/ChipSynth.lua | 53 ++++++++- src/core/Music.lua | 13 ++- src/core/chip_worker.lua | 19 ++- src/ui/gen2/OptionsMenu.lua | 5 + 9 files changed, 193 insertions(+), 268 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml delete mode 100644 .github/ISSUE_TEMPLATE/mod_request.yml create mode 100644 docs/tiled-map-editing.md diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml deleted file mode 100644 index 230d6713..00000000 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ /dev/null @@ -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 diff --git a/.github/ISSUE_TEMPLATE/mod_request.yml b/.github/ISSUE_TEMPLATE/mod_request.yml deleted file mode 100644 index abf7fa7b..00000000 --- a/.github/ISSUE_TEMPLATE/mod_request.yml +++ /dev/null @@ -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 diff --git a/README.md b/README.md index d3657bf4..630c6fc6 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/docs/tiled-map-editing.md b/docs/tiled-map-editing.md new file mode 100644 index 00000000..6ec9f7dd --- /dev/null +++ b/docs/tiled-map-editing.md @@ -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. diff --git a/src/core/ChipAudio.lua b/src/core/ChipAudio.lua index 2b16e6b6..c020301e 100644 --- a/src/core/ChipAudio.lua +++ b/src/core/ChipAudio.lua @@ -183,6 +183,9 @@ end -- --------------------------------------------------------------------------- local musicGen = 0 +-- bumps when SOUND flips so already-queued PCM (old pan) is dropped rather +-- than playing out the ~6s stall-tolerance queue (#1471) +local stereoEpoch = 0 function ChipAudio.playMusic(data, header, allowLoops) if not ensureWorker() then @@ -204,9 +207,11 @@ function ChipAudio.playMusic(data, header, allowLoops) allowLoops = allowLoops, audio = slimAudio(data), channelVolumes = ChipSynth.getChannelVolumes(), channelPitches = ChipSynth.getChannelPitches(), - stereo = ChipSynth.getStereo() }) + stereo = ChipSynth.getStereo(), + stereoEpoch = stereoEpoch }) currentMusic = { source = source, gen = gen, threaded = true, - started = false, finished = false } + started = false, finished = false, + stereoEpoch = stereoEpoch } -- playback starts in update() once the first buffer arrives (~1 frame) return source end @@ -236,6 +241,9 @@ local function updateThreaded() if not buf then break end if buf.gen ~= m.gen then -- stale buffer from a superseded song: drop it + elseif buf.stereoEpoch ~= nil and m.stereoEpoch ~= nil + and buf.stereoEpoch ~= m.stereoEpoch then + -- stale pan mix from before a live SOUND toggle (#1471) elseif buf.done then m.finished = true elseif buf.error then @@ -354,9 +362,45 @@ function ChipAudio.shutdown() workerReady = false end +function ChipAudio.currentSource() + return currentMusic and currentMusic.source +end + function ChipAudio.setStereo(enabled) + enabled = not not enabled + if ChipSynth.getStereo() == enabled then return end ChipSynth.setStereo(enabled) - pushChannelMix() + stereoEpoch = stereoEpoch + 1 + local m = currentMusic + if m and m.engine then + ChipSynth.applyStereo(m.engine) + end + if workerReady and cmdCh then + cmdCh:push({ cmd = "channelMix", + volumes = ChipSynth.getChannelVolumes(), + pitches = ChipSynth.getChannelPitches(), + stereo = enabled, + stereoEpoch = stereoEpoch }) + end + if not m then return end + pendingBuf = nil + if outCh then outCh:clear() end + m.stereoEpoch = stereoEpoch + -- QueueableSource cannot unqueue; swap so the ~6s stall-tolerance buffers + -- (mixed under the previous pan) do not have to play out first (#1471) + if not love.audio then return end + local ok, source = pcall( + love.audio.newQueueableSource, SAMPLE_RATE, 16, 2, MUSIC_BUFFER_COUNT) + if not ok or not source then return end + local old = m.source + m.source = source + m.started = false + if old then pcall(old.stop, old) end + if not m.threaded then + fillSync(MUSIC_FILL_INITIAL) + if not musicHeld then pcall(source.play, source) end + m.started = true + end end function ChipAudio.getStereo() diff --git a/src/core/ChipSynth.lua b/src/core/ChipSynth.lua index f870ddb5..df9173b5 100644 --- a/src/core/ChipSynth.lua +++ b/src/core/ChipSynth.lua @@ -294,6 +294,10 @@ function Channel.new(engine, spec, options) noiseSampling = false, -- Gen 2 toggle_noise condition = 0, -- Gen 2 set_condition / sound_jump_if tracks = tracks, -- Gen 2 CHANNEL_TRACKS (NR51 bits for this channel) + -- last Music_StereoPanning byte; remembered even while MONO so a live + -- SOUND toggle can re-apply it without restarting the song (#1471) + stereoPanning = nil, + forcePanning = false, -- ForceStereoPanning ($e4) ignores the SOUND option waveInstrument = 0, waveLevel = 1, perfectPitch = false, @@ -406,6 +410,20 @@ function Channel:pan() bit.band(self.engine.pan, mask) ~= 0 end +-- Recompute CHANNEL_TRACKS from the remembered panning byte and the live +-- STEREO flag. ForceStereoPanning stays put either way. +function Channel:applyStereoMix() + local mask = bit.lshift(1, self.hardware - 1) + local default = bit.bor(bit.lshift(mask, 4), mask) + if self.forcePanning then + return + elseif stereoEnabled and self.stereoPanning then + self.tracks = bit.band(self.stereoPanning, default) + else + self.tracks = default + end +end + function Channel:tone(ticks, register, volume, fade) if register >= 0x800 then return self:timedEvent({ silence = true }, ticks) @@ -756,6 +774,7 @@ function Channel:nextEventGen2() local mask = bit.lshift(1, self.hardware - 1) local default = bit.bor(bit.lshift(mask, 4), mask) self.tracks = bit.band(packed, default) + self.forcePanning = true elseif command == 0xE5 then -- volume (global master; ignored for mix) self:byte() elseif command == 0xE6 then -- pitch_offset (big-endian) @@ -778,8 +797,12 @@ function Channel:nextEventGen2() elseif command == 0xEE then -- unknownmusic0xee self:word() elseif command == 0xEF then - -- audio/engine.asm:1987 Music_StereoPanning: apply only when STEREO is on + -- audio/engine.asm:1987 Music_StereoPanning: apply only when STEREO is on. + -- The packed byte is kept either way so ChipSynth.applyStereo can honour + -- a live SOUND toggle mid-song (#1471). local packed = self:byte() + self.stereoPanning = packed + self.forcePanning = false if stereoEnabled then local mask = bit.lshift(1, self.hardware - 1) local default = bit.bor(bit.lshift(mask, 4), mask) @@ -1298,14 +1321,30 @@ function Engine:sampleStereo() local left, right = 0, 0 for _, channel in ipairs(self.channels) do local value = channel:sample() - local event = channel.event - if not event or event.panLeft ~= false then left = left + value end - if not event or event.panRight ~= false then right = right + value end + local panLeft, panRight + if self.generation == 2 then + -- live CHANNEL_TRACKS, not the pan baked into the current note, so a + -- SOUND toggle reaches the next synthesized sample (#1471) + panLeft, panRight = channel:pan() + else + local event = channel.event + panLeft = not event or event.panLeft ~= false + panRight = not event or event.panRight ~= false + end + if panLeft then left = left + value end + if panRight then right = right + value end end return analogOut(self, left, "hpfCapLeft", "lpfLeft"), analogOut(self, right, "hpfCapRight", "lpfRight") end +function Engine:applyStereo() + if self.generation ~= 2 then return end + for _, channel in ipairs(self.channels) do + channel:applyStereoMix() + end +end + function Engine:sampleChannel(number) local selected = 0 for _, channel in ipairs(self.channels) do @@ -1360,4 +1399,10 @@ ChipSynth.newEngine = Engine.new ChipSynth.soundData = soundData ChipSynth.renderEffectData = renderEffectData +function ChipSynth.applyStereo(engine) + if type(engine) == "table" and engine.applyStereo then + engine:applyStereo() + end +end + return ChipSynth diff --git a/src/core/Music.lua b/src/core/Music.lua index 142bf78f..920e8360 100644 --- a/src/core/Music.lua +++ b/src/core/Music.lua @@ -507,7 +507,18 @@ function Music.applyOptions(opts) Music.setVolumeLevel(opts and opts.musicVol or 7) Music.setFilterLevel(opts and opts.musicFilter or 0) -- engine/menus/options_menu.asm SOUND row (wOptions STEREO bit) - require("src.core.ChipAudio").setStereo(opts and opts.sound == "STEREO") + local ChipAudio = require("src.core.ChipAudio") + ChipAudio.setStereo(opts and opts.sound == "STEREO") + -- setStereo may swap the queueable source so the new pan is not sitting + -- behind already-mixed buffers; re-bind so volume/filter follow (#1471) + if state.chip then + local src = ChipAudio.currentSource() + if src then + state.source = src + applyVolume(src) + applyFilter(src) + end + end end local function sourceStopped(src) diff --git a/src/core/chip_worker.lua b/src/core/chip_worker.lua index c3bce302..21c59f8b 100644 --- a/src/core/chip_worker.lua +++ b/src/core/chip_worker.lua @@ -6,9 +6,10 @@ -- Protocol -- main thread pushes command tables onto the "chipaudio_cmd" -- channel and drains produced buffers off "chipaudio_out": -- cmd = "play" { gen, header, allowLoops, audio, --- channelVolumes?, channelPitches? } +-- channelVolumes?, channelPitches?, stereo?, stereoEpoch? } -- cmd = "stop" halt production --- cmd = "channelMix" { volumes, pitches } per-hw volume/pitch +-- cmd = "channelMix" { volumes, pitches, stereo?, stereoEpoch? } +-- stereoEpoch present: live SOUND toggle; drop lookahead -- cmd = "invalidate" drop the bank cache -- cmd = "quit" end the thread -- out buffers are tagged with the play's `gen` so the main thread can @@ -39,6 +40,7 @@ local gen = nil -- active song generation, or nil when stopped local engine = nil -- the ChipSynth engine producing the current song local finished = false -- the current song ran out (non-looping) local data = nil -- { audio = } for ROM bank/wave reads +local stereoEpoch = 0 -- matches ChipAudio; stale pan buffers are dropped local function handle(cmd) if cmd.cmd == "play" then @@ -56,6 +58,7 @@ local function handle(cmd) if cmd.stereo ~= nil then ChipSynth.setStereo(cmd.stereo) end + if cmd.stereoEpoch ~= nil then stereoEpoch = cmd.stereoEpoch end local ok, eng = pcall(ChipSynth.newEngine, data, cmd.header, { allowLoops = cmd.allowLoops }) if ok then @@ -73,6 +76,11 @@ local function handle(cmd) if cmd.volumes ~= nil then ChipSynth.setChannelVolumes(cmd.volumes) end if cmd.pitches ~= nil then ChipSynth.setChannelPitches(cmd.pitches) end if cmd.stereo ~= nil then ChipSynth.setStereo(cmd.stereo) end + if engine and cmd.stereo ~= nil then ChipSynth.applyStereo(engine) end + if cmd.stereoEpoch ~= nil then + stereoEpoch = cmd.stereoEpoch + outCh:clear() + end elseif cmd.cmd == "invalidate" then ChipSynth.invalidateBanks() elseif cmd.cmd == "quit" then @@ -95,12 +103,13 @@ while true do local activeGen = gen local ok, sd = pcall(ChipSynth.soundData, engine, BUF, 2) if not ok then - outCh:push({ gen = activeGen, error = tostring(sd) }) + outCh:push({ gen = activeGen, error = tostring(sd), + stereoEpoch = stereoEpoch }) finished = true else - outCh:push({ gen = activeGen, sd = sd }) + outCh:push({ gen = activeGen, sd = sd, stereoEpoch = stereoEpoch }) if engine:finished() then - outCh:push({ gen = activeGen, done = true }) + outCh:push({ gen = activeGen, done = true, stereoEpoch = stereoEpoch }) finished = true end end diff --git a/src/ui/gen2/OptionsMenu.lua b/src/ui/gen2/OptionsMenu.lua index 0ef01840..901f4d2a 100644 --- a/src/ui/gen2/OptionsMenu.lua +++ b/src/ui/gen2/OptionsMenu.lua @@ -355,6 +355,11 @@ function OptionsMenu:cycle(row, delta) if next_ < 1 then next_ = #row.values end if next_ > #row.values then next_ = 1 end self.options[row.key] = row.values[next_] + -- MUSIC VOL applies itself as it steps; SOUND has to as well, or the + -- pan sits on the current song until the next map change (#1471) + if row.key == "sound" then + require("src.core.Music").applyOptions(self.options) + end end function OptionsMenu:leave_() From def967a8f8023eb7896999a066d7ff3a83bfe7bc Mon Sep 17 00:00:00 2001 From: 1jamie Date: Tue, 18 Aug 2026 13:23:10 -0500 Subject: [PATCH 2/8] fix(render): replace per-entity grass overdraw with full-screen cell pass The previous drawCellBottom calls fired only for cells containing a tracked entity. While walking this worked acceptably because the sprite's sub-pixel tween kept the visual overlap plausible, but while standing still the sprite is pixel-aligned with the cell and the opaque leaf-edge pixels in the grass bottom row paint over the player's feet. This change removes the per-entity isGrassCell checks and replaces them with a single post-sprite pass that overdraws every visible grass cell. TileRenderer: - ensureWindow now builds grassCells (all paths) and grassBatch (DMG/SGB shader path) alongside winBatch during the existing tile scan loop. A grassSeen table deduplicates cells so each cx/cy pair is only recorded once despite having two bottom-row tiles. - drawGrassOverdraw: DMG/SGB draws the grassBatch under color0KeyShader in one call; GBC iterates grassCells and calls drawCellBottomRaw per cell (pre-keyed images can't share a SpriteBatch). - markGrassOverdrawRedraw: iterates grassCells and calls markCellBottomRedraw for the post-zone OBP-replay pass (GBC only). - releaseBatches cleans up grassBatch and grassCells. OverworldController (flat path): - Entity loop draws sprites only; grass overdraw fires once after the loop via drawGrassOverdraw + markGrassOverdrawRedraw. OverworldController (tilt path): - Grass cells are injected into the billboard sort queue keyed on the world-pixel foot of each cell's bottom tile row (cy*16+16), so they depth-sort correctly against entities at different y positions. Each grass cell billboards via drawCellBottomRaw inside the upright pass. Fixes: standing-in-tall-grass feet overdraw (Gen 2 confirmed, Gen 1 improved); NPCs and Pikachu follower in grass benefit automatically. Parity test: tests/parity_grass_seam.lua 10/10, engine 228/228. --- src/render/TileRenderer.lua | 64 +++++++++++++++++++++++++++++ src/world/OverworldController.lua | 67 ++++++++++++++++--------------- 2 files changed, 98 insertions(+), 33 deletions(-) diff --git a/src/render/TileRenderer.lua b/src/render/TileRenderer.lua index f616d252..6f46334e 100644 --- a/src/render/TileRenderer.lua +++ b/src/render/TileRenderer.lua @@ -760,6 +760,36 @@ function TileRenderer:markCellBottomRedraw(cx, cy, camX, camY, colors) end end +-- Draw the bottom tile row of every visible grass cell after sprites. +-- DMG/SGB: one SpriteBatch draw under the color-0-key shader. +-- GBC: per-cell draws using pre-keyed images (can't share one batch). +function TileRenderer:drawGrassOverdraw(camX, camY) + local ox, oy = -math.floor(camX), -math.floor(camY) + if self.gbcCtx then + if self.grassCells then + for _, c in ipairs(self.grassCells) do + self:drawCellBottomRaw(c[1], c[2], camX, camY) + end + end + else + local shader = getColor0KeyShader() + if shader then love.graphics.setShader(shader) end + if self.grassBatch then + love.graphics.draw(self.grassBatch, ox, oy) + end + if shader then love.graphics.setShader() end + end +end + +-- Queue every visible grass cell's bottom row for the post-zone +-- sprite-redraw pass (GBC OBP replay; mirrors markCellBottomRedraw). +function TileRenderer:markGrassOverdrawRedraw(camX, camY, colors) + if not self.grassCells then return end + for _, c in ipairs(self.grassCells) do + self:markCellBottomRedraw(c[1], c[2], camX, camY, colors) + end +end + local WINDOW_MARGIN = 8 -- tiles of slack kept around the view between refills function TileRenderer:ensureWindow(camX, camY, vw, vh) @@ -785,6 +815,16 @@ function TileRenderer:ensureWindow(camX, camY, vw, vh) self.winBatch = love.graphics.newSpriteBatch(self.image, 1024, "dynamic") end self.winBatch:clear() + -- grass-overdraw pass structures: a SpriteBatch for the DMG/SGB shader + -- path (one atlas, one shader draw call) and a plain list for the GBC + -- path (per-cell pre-keyed images can't share a single SpriteBatch). + if not self.gbcCtx then + if not self.grassBatch then + self.grassBatch = love.graphics.newSpriteBatch(self.image, 1024, "dynamic") + end + self.grassBatch:clear() + end + self.grassCells = {} local anims = self.anims for _, anim in ipairs(anims) do if not anim.batch then @@ -794,6 +834,9 @@ function TileRenderer:ensureWindow(camX, camY, vw, vh) end local map, quads = self.map, self.quads local claimedBy, aliasMap = self.claimedBy, self.aliasMap + -- track which grass cells we've already added so each cx/cy pair is only + -- recorded once even though its two bottom-row tiles share the same cell. + local grassSeen = {} for ty = ty0, ty1 - 1 do local by = math.floor(ty / 4) local ty4 = ty % 4 @@ -816,6 +859,25 @@ function TileRenderer:ensureWindow(camX, camY, vw, vh) anim.batch:add(wx, wy) end end + -- bottom tile row of a 2×2 cell (ty is odd) that is a grass cell: + -- record it for the post-sprite grass overdraw pass. + if ty % 2 == 1 then + local cx, cy = math.floor(tx / 2), math.floor(ty / 2) + local key = cy * 65536 + cx + if not grassSeen[key] and map:isGrassCell(cx, cy) then + grassSeen[key] = true + self.grassCells[#self.grassCells + 1] = { cx, cy } + -- DMG/SGB path: bake both tiles of the bottom row into the batch + if self.grassBatch then + -- left tile (tx = cx*2) + local lq = quads[map:tileAt(cx * 2, ty)] + if lq then self.grassBatch:add(lq, cx * 16, ty * 8) end + -- right tile (tx = cx*2+1) + local rq = quads[map:tileAt(cx * 2 + 1, ty)] + if rq then self.grassBatch:add(rq, cx * 16 + 8, ty * 8) end + end + end + end end end end @@ -891,6 +953,8 @@ end -- :release on eviction. function TileRenderer:releaseBatches() safeRelease(self.winBatch); self.winBatch = nil + safeRelease(self.grassBatch); self.grassBatch = nil + self.grassCells = nil safeRelease(self.borderFill); self.borderFill = nil safeRelease(self.borderQuad); self.borderQuad = nil -- shared shift-variant cache; only drop the reference diff --git a/src/world/OverworldController.lua b/src/world/OverworldController.lua index 0dbc0770..b93fa8bc 100644 --- a/src/world/OverworldController.lua +++ b/src/world/OverworldController.lua @@ -5303,25 +5303,16 @@ function OverworldState:drawWorld() if not ((self.flyAnim or self.flyArrive or self.playerHidden) and e == self.player) then e:draw(cam.x, cam.y) - -- tall grass overdraws the sprite's feet (GB sprite priority); - -- the overdraw is BG tiles, so it rides the shake offset too - love.graphics.setColor(1, 1, 1, 1) - if self.map:isGrassCell(e.cellX, e.cellY) then - self.map.renderer:drawCellBottom(e.cellX, e.cellY, cam.x, bgY) - if grassColors then - self.map.renderer:markCellBottomRedraw(e.cellX, e.cellY, - cam.x, bgY, grassColors) - end - end - if e.targetX and self.map:isGrassCell(e.targetX, e.targetY) then - self.map.renderer:drawCellBottom(e.targetX, e.targetY, cam.x, bgY) - if grassColors then - self.map.renderer:markCellBottomRedraw(e.targetX, e.targetY, - cam.x, bgY, grassColors) - end - end end end + -- tall grass overdraws every visible grass cell's feet row after all + -- sprites (GB sprite-priority parity). One pass regardless of how many + -- entities are on screen -- see TileRenderer:drawGrassOverdraw. + love.graphics.setColor(1, 1, 1, 1) + self.map.renderer:drawGrassOverdraw(cam.x, bgY) + if grassColors then + self.map.renderer:markGrassOverdrawRedraw(cam.x, bgY, grassColors) + end fxHeal() fxDust() fxCutTree() @@ -5357,6 +5348,18 @@ function OverworldState:drawWorld() items[#items + 1] = { y = e.py + 16, kind = "entity", e = e } end end + -- Inject grass-cell overdraw items into the same depth-sorted queue so + -- they occlude entities at lower y correctly (back-to-front by cell foot). + -- Each cell's foot y = cy*16 + 16 in world pixels (bottom of its two rows). + local grassCells = self.map.renderer.grassCells + if grassCells then + for _, c in ipairs(grassCells) do + local cx, cy = c[1], c[2] + -- world-pixel foot of the grass cell's bottom tile row + local cellFootY = (cy * 2 + 2) * 8 -- == cy*16+16 + items[#items + 1] = { y = cellFootY, kind = "grass", cx = cx, cy = cy } + end + end table.sort(items, function(a, b) return a.y < b.y end) for _, it in ipairs(items) do @@ -5368,6 +5371,20 @@ function OverworldState:drawWorld() local fy = g.npc.py - cam.y + g.oy + 16 self:billboard(fx, fy, vw, vh, zoneColorsAt(zones, fx, fy), false, function() g.npc:draw(cam.x - g.ox, cam.y - g.oy) end) + elseif it.kind == "grass" then + -- tall-grass bottom-row overdraw: billboarded at the cell's foot so + -- it depth-sorts correctly against any entity in the same y column. + -- bgY keeps the elevator-shake offset; drawCellBottomRaw lets the + -- billboard own the shader (color-0 keying baked into the keyed image + -- on GBC, or applied by drawCellBottom's shader on DMG/SGB). + local cx, cy = it.cx, it.cy + local fx = cx * 16 - cam.x + 8 -- horizontal centre of the cell + local fy = (cy * 2 + 2) * 8 - cam.y -- foot of the bottom tile row + local colors = zoneColorsAt(zones, fx, fy) + self:billboard(fx, fy, vw, vh, colors, true, function() + love.graphics.setColor(1, 1, 1, 1) + self.map.renderer:drawCellBottomRaw(cx, cy, cam.x, bgY) + end) else local e = it.e local fx = e.px - cam.x + 8 @@ -5375,22 +5392,6 @@ function OverworldState:drawWorld() local colors = zoneColorsAt(zones, fx, fy) self:billboard(fx, fy, vw, vh, colors, false, function() e:draw(cam.x, cam.y) end) - -- tall-grass feet overdraw glued to the sprite: same anchor + depth - -- so it keeps hiding the feet, color-0-keyed palette so its white - -- gaps still show the sprite through (drawCellBottomRaw lets the - -- billboard own the shader; bgY keeps the elevator-shake offset). - if self.map:isGrassCell(e.cellX, e.cellY) then - self:billboard(fx, fy, vw, vh, colors, true, function() - love.graphics.setColor(1, 1, 1, 1) - self.map.renderer:drawCellBottomRaw(e.cellX, e.cellY, cam.x, bgY) - end) - end - if e.targetX and self.map:isGrassCell(e.targetX, e.targetY) then - self:billboard(fx, fy, vw, vh, colors, true, function() - love.graphics.setColor(1, 1, 1, 1) - self.map.renderer:drawCellBottomRaw(e.targetX, e.targetY, cam.x, bgY) - end) - end end end From 99806ead25e1f749c183139f2680250c57f032b3 Mon Sep 17 00:00:00 2001 From: bryanthaboi Date: Tue, 18 Aug 2026 15:49:47 -0400 Subject: [PATCH 3/8] Potentially CLOSES #1310, CLOSES #1005, CLOSES #1291 --- main.lua | 19 + .../love/src/jni/love/src/common/android.cpp | 70 + .../love/src/modules/audio/openal/Audio.cpp | 123 +- .../jni/love/src/modules/audio/openal/Audio.h | 19 +- .../oboe/.github/ISSUE_TEMPLATE/bug_report.md | 40 + .../love/src/jni/oboe/.github/dependabot.yml | 36 + .../jni/oboe/.github/workflows/build-ci.yml | 38 + .../oboe/.github/workflows/update-docs.yml | 24 + mobile/android/love/src/jni/oboe/.gitignore | 2 + mobile/android/love/src/jni/oboe/Android.mk | 31 +- .../android/love/src/jni/oboe/CMakeLists.txt | 26 +- mobile/android/love/src/jni/oboe/CONTRIBUTING | 1 - mobile/android/love/src/jni/oboe/Doxyfile | 4 +- .../MODULE_LICENSE_APACHE2} | 0 mobile/android/love/src/jni/oboe/README | 1 - mobile/android/love/src/jni/oboe/README.md | 27 +- .../src/jni/oboe/apps/OboeTester/.gitignore | 2 + .../oboe/apps/OboeTester/app/CMakeLists.txt | 8 +- .../jni/oboe/apps/OboeTester/app/build.gradle | 26 +- .../app/src/main/AndroidManifest.xml | 169 +- .../app/src/main/cpp/AudioStreamGateway.cpp | 15 +- .../app/src/main/cpp/AudioStreamGateway.h | 19 +- .../app/src/main/cpp/FormatConverterBox.cpp | 91 + .../app/src/main/cpp/FormatConverterBox.h | 102 + .../app/src/main/cpp/FullDuplexAnalyzer.cpp | 34 +- .../app/src/main/cpp/FullDuplexAnalyzer.h | 32 +- .../app/src/main/cpp/FullDuplexEcho.cpp | 38 +- .../app/src/main/cpp/FullDuplexEcho.h | 18 +- .../app/src/main/cpp/FullDuplexGlitches.h | 53 - .../app/src/main/cpp/FullDuplexLatency.cpp | 44 - .../app/src/main/cpp/FullDuplexLatency.h | 65 - .../app/src/main/cpp/FullDuplexStream.cpp | 137 - .../app/src/main/cpp/FullDuplexStream.h | 115 - .../cpp/FullDuplexStreamWithConversion.cpp | 61 + .../main/cpp/FullDuplexStreamWithConversion.h | 61 + .../main/cpp/InputStreamCallbackAnalyzer.cpp | 48 +- .../main/cpp/InputStreamCallbackAnalyzer.h | 37 +- .../src/main/cpp/InterpolatingDelayLine.cpp | 3 +- .../app/src/main/cpp/InterpolatingDelayLine.h | 3 - .../app/src/main/cpp/NativeAudioContext.cpp | 318 +- .../app/src/main/cpp/NativeAudioContext.h | 453 +- .../src/main/cpp/OboeStreamCallbackProxy.cpp | 119 +- .../src/main/cpp/OboeStreamCallbackProxy.h | 217 +- .../src/main/cpp/OboeTesterStreamCallback.cpp | 86 + .../src/main/cpp/OboeTesterStreamCallback.h | 58 + .../{FullDuplexGlitches.cpp => OboeTools.h} | 13 +- .../app/src/main/cpp/SawPingGenerator.cpp | 15 +- .../app/src/main/cpp/SawPingGenerator.h | 4 +- .../app/src/main/cpp/TestColdStartLatency.cpp | 118 + .../app/src/main/cpp/TestColdStartLatency.h | 76 + .../app/src/main/cpp/TestErrorCallback.cpp | 75 + .../app/src/main/cpp/TestErrorCallback.h | 113 + .../app/src/main/cpp/TestRapidCycle.cpp | 97 + .../app/src/main/cpp/TestRapidCycle.h | 67 + .../app/src/main/cpp/TestRoutingCrash.cpp | 111 + .../app/src/main/cpp/TestRoutingCrash.h | 67 + .../src/main/cpp/analyzer/BaseSineAnalyzer.h | 240 + .../src/main/cpp/analyzer/DataPathAnalyzer.h | 106 + .../src/main/cpp/analyzer/GlitchAnalyzer.h | 392 +- .../src/main/cpp/analyzer/InfiniteRecording.h | 5 +- .../src/main/cpp/analyzer/LatencyAnalyzer.h | 549 +- .../src/main/cpp/analyzer/ManchesterEncoder.h | 11 +- .../app/src/main/cpp/analyzer/PeakDetector.h | 26 +- .../app/src/main/cpp/analyzer/PseudoRandom.h | 8 +- .../main/cpp/analyzer/RandomPulseGenerator.h | 4 +- .../cpp/analyzer/RoundedManchesterEncoder.h | 39 +- .../main/cpp/flowunits/ExponentialShape.cpp | 2 +- .../src/main/cpp/flowunits/ExponentialShape.h | 2 +- .../src/main/cpp/flowunits/LinearShape.cpp | 4 +- .../app/src/main/cpp/flowunits/LinearShape.h | 2 +- .../src/main/cpp/flowunits/OscillatorBase.cpp | 2 +- .../src/main/cpp/flowunits/OscillatorBase.h | 8 +- .../app/src/main/cpp/flowunits/WhiteNoise.cpp | 32 + .../app/src/main/cpp/flowunits/WhiteNoise.h | 57 + .../app/src/main/cpp/jni-bridge.cpp | 612 +- .../app/src/main/cpp/synth/BiquadFilter.h | 163 + .../main/cpp/synth/DifferentiatedParabola.h | 78 + .../app/src/main/cpp/synth/EnvelopeADSR.h | 229 + .../app/src/main/cpp/synth/IncludeMeOnce.h | 36 + .../app/src/main/cpp/synth/LookupTable.h | 72 + .../app/src/main/cpp/synth/PitchToFrequency.h | 104 + .../src/main/cpp/synth/SawtoothOscillator.h | 80 + .../main/cpp/synth/SawtoothOscillatorDPW.h | 53 + .../app/src/main/cpp/synth/SimpleVoice.h | 137 + .../app/src/main/cpp/synth/SineOscillator.h | 44 + .../src/main/cpp/synth/SquareOscillatorDPW.h | 73 + .../app/src/main/cpp/synth/SynthTools.h | 173 + .../app/src/main/cpp/synth/Synthesizer.h | 135 + .../app/src/main/cpp/synth/UnitGenerator.h | 55 + .../app/src/main/cpp/synth/VoiceBase.h | 58 + .../app/src/main/cpp/util/WaveFileWriter.cpp | 28 +- .../app/src/main/cpp/util/WaveFileWriter.h | 49 +- .../app/src/main/ic_launcher-playstore.png | Bin 0 -> 31684 bytes .../oboe/manualtest/AudioMidiTester.java | 203 - .../oboe/manualtest/AutoGlitchActivity.java | 328 - .../oboe/manualtest/BufferSizeView.java | 143 - .../oboe/manualtest/ManualGlitchActivity.java | 286 - .../sample/oboe/manualtest/NativeEngine.java | 10 - .../manualtest/RoundTripLatencyActivity.java | 395 - .../oboe/manualtest/StreamConfiguration.java | 355 - .../manualtest/StreamConfigurationView.java | 464 - .../oboe/manualtest/TapToToneActivity.java | 386 - .../oboe/manualtest/TestAudioActivity.java | 486 - .../manualtest/TestDisconnectActivity.java | 482 - .../oboe/manualtest/TestOutputActivity.java | 121 - .../audio_device/AudioDeviceAdapter.java | 9 +- .../AudioDeviceInfoConverter.java | 97 +- .../audio_device/AudioDeviceListEntry.java | 20 +- .../audio_device/AudioDeviceSpinner.java | 19 +- .../CommunicationDeviceSpinner.java | 126 + .../mobileer/miditools/synth/SynthEngine.java | 3 +- .../oboetester}/AnalyzerActivity.java | 128 +- .../oboetester/AudioForegroundService.java | 80 + .../oboetester}/AudioInputTester.java | 2 +- .../oboetester}/AudioOutputTester.java | 21 +- .../mobileer/oboetester/AudioQueryTools.java | 176 + .../oboetester}/AudioRecordThread.java | 15 +- .../oboetester}/AudioStreamBase.java | 128 +- .../oboetester}/AudioStreamTester.java | 5 +- .../oboetester/AutomatedGlitchActivity.java | 153 + .../oboetester/AutomatedTestRunner.java | 338 + .../oboetester/BaseAutoGlitchActivity.java | 613 + .../oboetester/BaseOboeTesterActivity.java | 108 + .../mobileer/oboetester/BufferSizeView.java | 222 + .../oboetester/CachedTextViewLog.java | 72 + .../oboetester}/CircularCaptureBuffer.java | 2 +- .../oboetester/CommunicationDeviceView.java | 211 + .../oboetester/DeviceReportActivity.java | 347 + .../mobileer/oboetester/DoubleStatistics.java | 68 + .../oboetester/DynamicWorkloadActivity.java | 473 + .../oboetester}/EchoActivity.java | 73 +- .../oboetester}/ExponentialTaper.java | 2 +- .../oboetester/ExternalFileWriter.java | 55 + .../oboetester/ExternalTapToToneActivity.java | 98 + .../oboetester/ExtraTestsActivity.java | 45 + .../oboetester}/FastButton.java | 9 +- .../oboetester}/GlitchActivity.java | 263 +- .../oboetester}/InputMarginView.java | 6 +- .../oboetester/IntentBasedTestSupport.java | 377 + .../oboetester}/MainActivity.java | 196 +- .../oboetester/ManualGlitchActivity.java | 291 + .../oboetester/MicrophoneInfoConverter.java | 82 + .../mobileer/oboetester/MidiTapTester.java | 119 + .../mobileer/oboetester/MultiLineChart.java | 254 + .../com/mobileer/oboetester/NativeEngine.java | 18 + .../mobileer/oboetester/NativeSniffer.java | 61 + .../oboetester}/OboeAudioInputStream.java | 2 +- .../oboetester}/OboeAudioOutputStream.java | 14 +- .../oboetester}/OboeAudioStream.java | 152 +- .../oboetester}/RecorderActivity.java | 8 +- .../oboetester/RoundTripLatencyActivity.java | 780 + .../oboetester/StreamConfiguration.java | 846 + .../oboetester/StreamConfigurationView.java | 653 + .../oboetester}/TapLatencyAnalyser.java | 48 +- .../oboetester/TapToToneActivity.java | 305 + .../mobileer/oboetester/TapToToneTester.java | 207 + .../oboetester/TestAudioActivity.java | 919 + .../TestColdStartLatencyActivity.java | 209 + .../oboetester/TestDataPathsActivity.java | 984 + .../oboetester/TestDisconnectActivity.java | 566 + .../oboetester/TestErrorCallbackActivity.java | 101 + .../oboetester}/TestInputActivity.java | 143 +- .../oboetester/TestOutputActivity.java | 228 + .../oboetester}/TestOutputActivityBase.java | 50 +- .../oboetester/TestPlugLatencyActivity.java | 286 + .../oboetester/TestRapidCycleActivity.java | 155 + .../TestRouteDuringCallbackActivity.java | 178 + .../oboetester}/VolumeBarView.java | 53 +- .../oboetester}/WaveformView.java | 62 +- .../oboetester}/WorkloadView.java | 49 +- .../res/drawable/ic_launcher_background.xml | 74 + .../res/drawable/ic_launcher_foreground.xml | 15 + .../src/main/res/drawable/ic_notification.xml | 83 + .../res/layout/activity_auto_glitches.xml | 64 +- .../layout/activity_cold_start_latency.xml | 157 + .../main/res/layout/activity_data_paths.xml | 103 + .../res/layout/activity_device_report.xml | 28 + .../res/layout/activity_dynamic_workload.xml | 127 + .../app/src/main/res/layout/activity_echo.xml | 172 +- .../res/layout/activity_error_callback.xml | 35 + .../layout/activity_external_tap_to_tone.xml | 64 + .../main/res/layout/activity_extra_tests.xml | 95 + .../app/src/main/res/layout/activity_main.xml | 261 +- .../res/layout/activity_manual_glitches.xml | 56 +- .../main/res/layout/activity_rapid_cycle.xml | 79 + .../src/main/res/layout/activity_recorder.xml | 26 +- .../res/layout/activity_routing_crash.xml | 75 + .../main/res/layout/activity_rt_latency.xml | 59 +- .../main/res/layout/activity_tap_to_tone.xml | 52 +- .../res/layout/activity_test_disconnect.xml | 77 +- .../main/res/layout/activity_test_input.xml | 53 +- .../main/res/layout/activity_test_output.xml | 222 +- .../res/layout/activity_test_plug_latency.xml | 47 + .../app/src/main/res/layout/audio_devices.xml | 6 +- .../src/main/res/layout/auto_test_runner.xml | 80 + .../src/main/res/layout/buffer_size_view.xml | 38 +- .../src/main/res/layout/comm_device_view.xml | 59 + .../src/main/res/layout/input_margin_view.xml | 2 +- .../main/res/layout/merge_audio_common.xml | 92 +- .../main/res/layout/merge_audio_simple.xml | 53 + .../main/res/layout/sample_fast_button.xml | 4 +- .../res/layout/sample_multi_line_chart.xml | 16 + .../app/src/main/res/layout/stream_config.xml | 161 +- .../app/src/main/res/layout/workload_view.xml | 2 +- .../app/src/main/res/menu/menu_main.xml | 10 +- .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 3418 -> 0 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 5353 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 2206 -> 0 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 3342 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 4842 -> 0 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 7478 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 7718 -> 0 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 12018 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 9610 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 16879 bytes .../app/src/main/res/values-v21/styles.xml | 5 - .../app/src/main/res/values/arrays.xml | 2 +- .../src/main/res/values/attrs_fast_button.xml | 8 +- .../res/values/attrs_multi_line_chart.xml | 10 + .../src/main/res/values/attrs_waveform.xml | 2 +- .../app/src/main/res/values/colors.xml | 6 + .../app/src/main/res/values/dimens.xml | 2 + .../app/src/main/res/values/strings.xml | 178 +- .../app/src/main/res/values/styles.xml | 7 +- .../main/res/xml/button_color_selector.xml | 5 + .../app/src/main/res/xml/provider_paths.xml | 2 +- .../src/main/res/xml/service_device_info.xml | 3 +- .../src/jni/oboe/apps/OboeTester/build.gradle | 6 +- .../apps/OboeTester/docs/AutomatedTesting.md | 258 +- .../jni/oboe/apps/OboeTester/docs/Build.md | 9 +- .../apps/OboeTester/docs/PrivacyPolicy.md | 39 + .../jni/oboe/apps/OboeTester/docs/README.md | 8 +- .../oboe/apps/OboeTester/docs/TestOutput.md | 30 + .../jni/oboe/apps/OboeTester/docs/Usage.md | 124 +- .../oboe/apps/OboeTester/gradle.properties | 24 +- .../gradle/wrapper/gradle-wrapper.properties | 4 +- .../love/src/jni/oboe/apps/OboeTester/gradlew | 0 .../apps/OboeTester/scripts/dsp_timing.py | 6 +- .../jni/oboe/apps/fxlab/app/CMakeLists.txt | 2 +- .../src/jni/oboe/apps/fxlab/app/build.gradle | 12 +- .../fxlab/app/src/main/AndroidManifest.xml | 9 +- .../cpp/effects/descrip/TremoloDescription.h | 2 +- .../fxlab/app/src/main/cpp/native-lib.cpp | 1 + .../mobileer/androidfxlab/EffectsAdapter.kt | 5 +- .../com/mobileer/androidfxlab/MainActivity.kt | 23 +- .../mobileer/androidfxlab/NativeInterface.kt | 0 .../mobileer/androidfxlab/datatype/Effect.kt | 2 +- .../datatype/EffectDescription.kt | 0 .../androidfxlab/datatype/ParamDescription.kt | 2 +- .../app/src/main/res/layout/effect_header.xml | 2 +- .../app/src/main/res/layout/effect_view.xml | 2 +- .../app/src/main/res/layout/param_seek.xml | 2 +- .../fxlab/app/src/main/res/menu/add_menu.xml | 2 +- .../love/src/jni/oboe/apps/fxlab/build.gradle | 4 +- .../src/jni/oboe/apps/fxlab/gradle.properties | 3 + .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../love/src/jni/oboe/apps/fxlab/gradlew | 0 .../love/src/jni/oboe/build_all_android.sh | 5 +- .../src/jni/oboe/docs/AndroidAudioHistory.md | 13 +- .../love/src/jni/oboe/docs/AppsUsingOboe.md | 31 - .../love/src/jni/oboe/docs/ChangeLog.md | 30 - mobile/android/love/src/jni/oboe/docs/FAQ.md | 19 +- .../love/src/jni/oboe/docs/FullGuide.md | 131 +- .../love/src/jni/oboe/docs/GettingStarted.md | 183 +- .../src/jni/oboe/docs/OpenSLESMigration.md | 169 + .../love/src/jni/oboe/docs/PrivacyPolicy.md | 11 + .../android/love/src/jni/oboe/docs/README.md | 14 +- .../android/love/src/jni/oboe/docs/index.html | 11 - .../love/src/jni/oboe/docs/notes/README.md | 7 - .../src/jni/oboe/docs/notes/disconnect.md | 55 - .../love/src/jni/oboe/docs/notes/effects.md | 48 - .../love/src/jni/oboe/docs/notes/rlsbuffer.md | 71 - .../reference/_audio_stream_8h_source.html | 138 - .../_audio_stream_base_8h_source.html | 133 - .../_audio_stream_builder_8h_source.html | 137 - .../_audio_stream_callback_8h_source.html | 90 - .../reference/_definitions_8h_source.html | 146 - .../reference/_latency_tuner_8h_source.html | 90 - .../oboe/docs/reference/_oboe_8h_source.html | 82 - .../_result_with_value_8h_source.html | 93 - .../_stabilized_callback_8h_source.html | 93 - .../docs/reference/_utilities_8h_source.html | 89 - .../docs/reference/_version_8h_source.html | 89 - .../jni/oboe/docs/reference/annotated.html | 94 - .../love/src/jni/oboe/docs/reference/bc_s.png | Bin 676 -> 0 bytes .../love/src/jni/oboe/docs/reference/bdwn.png | Bin 147 -> 0 bytes .../src/jni/oboe/docs/reference/classes.html | 97 - .../classoboe_1_1_audio_stream-members.html | 172 - .../reference/classoboe_1_1_audio_stream.html | 1590 - .../reference/classoboe_1_1_audio_stream.png | Bin 629 -> 0 bytes ...assoboe_1_1_audio_stream_base-members.html | 126 - .../classoboe_1_1_audio_stream_base.html | 1085 - .../classoboe_1_1_audio_stream_base.png | Bin 892 -> 0 bytes ...oboe_1_1_audio_stream_builder-members.html | 152 - .../classoboe_1_1_audio_stream_builder.html | 1014 - .../classoboe_1_1_audio_stream_builder.png | Bin 673 -> 0 bytes ...boe_1_1_audio_stream_callback-members.html | 89 - .../classoboe_1_1_audio_stream_callback.html | 295 - .../classoboe_1_1_audio_stream_callback.png | Bin 693 -> 0 bytes ...boe_1_1_default_stream_values-members.html | 88 - .../classoboe_1_1_default_stream_values.html | 181 - .../classoboe_1_1_latency_tuner-members.html | 90 - .../classoboe_1_1_latency_tuner.html | 245 - ...assoboe_1_1_result_with_value-members.html | 93 - .../classoboe_1_1_result_with_value.html | 368 - ...soboe_1_1_stabilized_callback-members.html | 90 - .../classoboe_1_1_stabilized_callback.html | 293 - .../classoboe_1_1_stabilized_callback.png | Bin 686 -> 0 bytes .../src/jni/oboe/docs/reference/closed.png | Bin 132 -> 0 bytes .../jni/oboe/docs/reference/deprecated.html | 83 - .../dir_768f6301d9838e45d679001914ab2803.html | 82 - .../dir_d44c64559bbebec7f509842c48db8b23.html | 86 - .../love/src/jni/oboe/docs/reference/doc.png | Bin 746 -> 0 bytes .../src/jni/oboe/docs/reference/doxygen.css | 1764 - .../src/jni/oboe/docs/reference/doxygen.png | Bin 3779 -> 0 bytes .../jni/oboe/docs/reference/dynsections.js | 120 - .../src/jni/oboe/docs/reference/files.html | 95 - .../jni/oboe/docs/reference/folderclosed.png | Bin 616 -> 0 bytes .../jni/oboe/docs/reference/folderopen.png | Bin 597 -> 0 bytes .../jni/oboe/docs/reference/functions.html | 512 - .../oboe/docs/reference/functions_func.html | 426 - .../oboe/docs/reference/functions_vars.html | 154 - .../jni/oboe/docs/reference/hierarchy.html | 93 - .../src/jni/oboe/docs/reference/index.html | 80 - .../src/jni/oboe/docs/reference/jquery.js | 87 - .../love/src/jni/oboe/docs/reference/menu.js | 50 - .../src/jni/oboe/docs/reference/menudata.js | 90 - .../oboe/docs/reference/namespacemembers.html | 253 - .../docs/reference/namespacemembers_enum.html | 118 - .../docs/reference/namespacemembers_eval.html | 121 - .../docs/reference/namespacemembers_func.html | 94 - .../docs/reference/namespacemembers_vars.html | 94 - .../oboe/docs/reference/namespaceoboe.html | 1008 - .../jni/oboe/docs/reference/namespaces.html | 83 - .../src/jni/oboe/docs/reference/nav_f.png | Bin 153 -> 0 bytes .../src/jni/oboe/docs/reference/nav_g.png | Bin 95 -> 0 bytes .../src/jni/oboe/docs/reference/nav_h.png | Bin 98 -> 0 bytes .../love/src/jni/oboe/docs/reference/open.png | Bin 123 -> 0 bytes .../src/jni/oboe/docs/reference/pages.html | 83 - .../jni/oboe/docs/reference/search/all_0.html | 30 - .../jni/oboe/docs/reference/search/all_0.js | 17 - .../jni/oboe/docs/reference/search/all_1.html | 30 - .../jni/oboe/docs/reference/search/all_1.js | 4 - .../oboe/docs/reference/search/all_10.html | 30 - .../jni/oboe/docs/reference/search/all_10.js | 5 - .../oboe/docs/reference/search/all_11.html | 30 - .../jni/oboe/docs/reference/search/all_11.js | 9 - .../oboe/docs/reference/search/all_12.html | 30 - .../jni/oboe/docs/reference/search/all_12.js | 9 - .../oboe/docs/reference/search/all_13.html | 30 - .../jni/oboe/docs/reference/search/all_13.js | 9 - .../jni/oboe/docs/reference/search/all_2.html | 30 - .../jni/oboe/docs/reference/search/all_2.js | 13 - .../jni/oboe/docs/reference/search/all_3.html | 30 - .../jni/oboe/docs/reference/search/all_3.js | 7 - .../jni/oboe/docs/reference/search/all_4.html | 30 - .../jni/oboe/docs/reference/search/all_4.js | 5 - .../jni/oboe/docs/reference/search/all_5.html | 30 - .../jni/oboe/docs/reference/search/all_5.js | 9 - .../jni/oboe/docs/reference/search/all_6.html | 30 - .../jni/oboe/docs/reference/search/all_6.js | 33 - .../jni/oboe/docs/reference/search/all_7.html | 30 - .../jni/oboe/docs/reference/search/all_7.js | 14 - .../jni/oboe/docs/reference/search/all_8.html | 30 - .../jni/oboe/docs/reference/search/all_8.js | 9 - .../jni/oboe/docs/reference/search/all_9.html | 30 - .../jni/oboe/docs/reference/search/all_9.js | 6 - .../jni/oboe/docs/reference/search/all_a.html | 30 - .../jni/oboe/docs/reference/search/all_a.js | 27 - .../jni/oboe/docs/reference/search/all_b.html | 30 - .../jni/oboe/docs/reference/search/all_b.js | 8 - .../jni/oboe/docs/reference/search/all_c.html | 30 - .../jni/oboe/docs/reference/search/all_c.js | 18 - .../jni/oboe/docs/reference/search/all_d.html | 30 - .../jni/oboe/docs/reference/search/all_d.js | 7 - .../jni/oboe/docs/reference/search/all_e.html | 30 - .../jni/oboe/docs/reference/search/all_e.js | 11 - .../jni/oboe/docs/reference/search/all_f.html | 30 - .../jni/oboe/docs/reference/search/all_f.js | 36 - .../oboe/docs/reference/search/classes_0.html | 30 - .../oboe/docs/reference/search/classes_0.js | 7 - .../oboe/docs/reference/search/classes_1.html | 30 - .../oboe/docs/reference/search/classes_1.js | 4 - .../oboe/docs/reference/search/classes_2.html | 30 - .../oboe/docs/reference/search/classes_2.js | 4 - .../oboe/docs/reference/search/classes_3.html | 30 - .../oboe/docs/reference/search/classes_3.js | 4 - .../oboe/docs/reference/search/classes_4.html | 30 - .../oboe/docs/reference/search/classes_4.js | 4 - .../oboe/docs/reference/search/classes_5.html | 30 - .../oboe/docs/reference/search/classes_5.js | 5 - .../oboe/docs/reference/search/classes_6.html | 30 - .../oboe/docs/reference/search/classes_6.js | 4 - .../jni/oboe/docs/reference/search/close.png | Bin 273 -> 0 bytes .../oboe/docs/reference/search/enums_0.html | 30 - .../jni/oboe/docs/reference/search/enums_0.js | 5 - .../oboe/docs/reference/search/enums_1.html | 30 - .../jni/oboe/docs/reference/search/enums_1.js | 5 - .../oboe/docs/reference/search/enums_2.html | 30 - .../jni/oboe/docs/reference/search/enums_2.js | 5 - .../oboe/docs/reference/search/enums_3.html | 30 - .../jni/oboe/docs/reference/search/enums_3.js | 4 - .../oboe/docs/reference/search/enums_4.html | 30 - .../jni/oboe/docs/reference/search/enums_4.js | 4 - .../oboe/docs/reference/search/enums_5.html | 30 - .../jni/oboe/docs/reference/search/enums_5.js | 4 - .../oboe/docs/reference/search/enums_6.html | 30 - .../jni/oboe/docs/reference/search/enums_6.js | 7 - .../oboe/docs/reference/search/enums_7.html | 30 - .../jni/oboe/docs/reference/search/enums_7.js | 4 - .../docs/reference/search/enumvalues_0.html | 30 - .../docs/reference/search/enumvalues_0.js | 10 - .../docs/reference/search/enumvalues_1.html | 30 - .../docs/reference/search/enumvalues_1.js | 4 - .../docs/reference/search/enumvalues_2.html | 30 - .../docs/reference/search/enumvalues_2.js | 4 - .../docs/reference/search/enumvalues_3.html | 30 - .../docs/reference/search/enumvalues_3.js | 4 - .../docs/reference/search/enumvalues_4.html | 30 - .../docs/reference/search/enumvalues_4.js | 5 - .../docs/reference/search/enumvalues_5.html | 30 - .../docs/reference/search/enumvalues_5.js | 5 - .../docs/reference/search/enumvalues_6.html | 30 - .../docs/reference/search/enumvalues_6.js | 6 - .../docs/reference/search/enumvalues_7.html | 30 - .../docs/reference/search/enumvalues_7.js | 4 - .../docs/reference/search/enumvalues_8.html | 30 - .../docs/reference/search/enumvalues_8.js | 7 - .../docs/reference/search/enumvalues_9.html | 30 - .../docs/reference/search/enumvalues_9.js | 7 - .../docs/reference/search/enumvalues_a.html | 30 - .../docs/reference/search/enumvalues_a.js | 5 - .../docs/reference/search/enumvalues_b.html | 30 - .../docs/reference/search/enumvalues_b.js | 4 - .../docs/reference/search/enumvalues_c.html | 30 - .../docs/reference/search/enumvalues_c.js | 7 - .../docs/reference/search/enumvalues_d.html | 30 - .../docs/reference/search/enumvalues_d.js | 5 - .../docs/reference/search/enumvalues_e.html | 30 - .../docs/reference/search/enumvalues_e.js | 7 - .../docs/reference/search/functions_0.html | 30 - .../oboe/docs/reference/search/functions_0.js | 5 - .../docs/reference/search/functions_1.html | 30 - .../oboe/docs/reference/search/functions_1.js | 10 - .../docs/reference/search/functions_2.html | 30 - .../oboe/docs/reference/search/functions_2.js | 4 - .../docs/reference/search/functions_3.html | 30 - .../oboe/docs/reference/search/functions_3.js | 5 - .../docs/reference/search/functions_4.html | 30 - .../oboe/docs/reference/search/functions_4.js | 31 - .../docs/reference/search/functions_5.html | 30 - .../oboe/docs/reference/search/functions_5.js | 10 - .../docs/reference/search/functions_6.html | 30 - .../oboe/docs/reference/search/functions_6.js | 5 - .../docs/reference/search/functions_7.html | 30 - .../oboe/docs/reference/search/functions_7.js | 15 - .../docs/reference/search/functions_8.html | 30 - .../oboe/docs/reference/search/functions_8.js | 4 - .../docs/reference/search/functions_9.html | 30 - .../oboe/docs/reference/search/functions_9.js | 10 - .../docs/reference/search/functions_a.html | 30 - .../oboe/docs/reference/search/functions_a.js | 25 - .../docs/reference/search/functions_b.html | 30 - .../oboe/docs/reference/search/functions_b.js | 4 - .../docs/reference/search/functions_c.html | 30 - .../oboe/docs/reference/search/functions_c.js | 6 - .../docs/reference/search/functions_d.html | 30 - .../oboe/docs/reference/search/functions_d.js | 4 - .../docs/reference/search/functions_e.html | 30 - .../oboe/docs/reference/search/functions_e.js | 9 - .../oboe/docs/reference/search/mag_sel.png | Bin 465 -> 0 bytes .../docs/reference/search/namespaces_0.html | 30 - .../docs/reference/search/namespaces_0.js | 4 - .../oboe/docs/reference/search/nomatches.html | 12 - .../oboe/docs/reference/search/pages_0.html | 30 - .../jni/oboe/docs/reference/search/pages_0.js | 4 - .../oboe/docs/reference/search/pages_1.html | 30 - .../jni/oboe/docs/reference/search/pages_1.js | 4 - .../jni/oboe/docs/reference/search/search.css | 271 - .../jni/oboe/docs/reference/search/search.js | 814 - .../oboe/docs/reference/search/search_l.png | Bin 567 -> 0 bytes .../oboe/docs/reference/search/search_m.png | Bin 158 -> 0 bytes .../oboe/docs/reference/search/search_r.png | Bin 553 -> 0 bytes .../oboe/docs/reference/search/searchdata.js | 36 - .../docs/reference/search/variables_0.html | 30 - .../oboe/docs/reference/search/variables_0.js | 4 - .../docs/reference/search/variables_1.html | 30 - .../oboe/docs/reference/search/variables_1.js | 4 - .../docs/reference/search/variables_2.html | 30 - .../oboe/docs/reference/search/variables_2.js | 9 - .../docs/reference/search/variables_3.html | 30 - .../oboe/docs/reference/search/variables_3.js | 23 - .../docs/reference/search/variables_4.html | 30 - .../oboe/docs/reference/search/variables_4.js | 4 - .../docs/reference/search/variables_5.html | 30 - .../oboe/docs/reference/search/variables_5.js | 4 - .../docs/reference/search/variables_6.html | 30 - .../oboe/docs/reference/search/variables_6.js | 4 - .../docs/reference/search/variables_7.html | 30 - .../oboe/docs/reference/search/variables_7.js | 4 - .../src/jni/oboe/docs/reference/splitbar.png | Bin 314 -> 0 bytes ...tructoboe_1_1_frame_timestamp-members.html | 87 - .../structoboe_1_1_frame_timestamp.html | 102 - ...oe_1_1_stream_deleter_functor-members.html | 86 - ...structoboe_1_1_stream_deleter_functor.html | 99 - .../structoboe_1_1_version-members.html | 90 - .../reference/structoboe_1_1_version.html | 222 - .../src/jni/oboe/docs/reference/sync_off.png | Bin 853 -> 0 bytes .../src/jni/oboe/docs/reference/sync_on.png | Bin 845 -> 0 bytes .../src/jni/oboe/docs/reference/tab_a.png | Bin 142 -> 0 bytes .../src/jni/oboe/docs/reference/tab_b.png | Bin 169 -> 0 bytes .../src/jni/oboe/docs/reference/tab_h.png | Bin 177 -> 0 bytes .../src/jni/oboe/docs/reference/tab_s.png | Bin 184 -> 0 bytes .../love/src/jni/oboe/docs/reference/tabs.css | 1 - .../{src/common => include/oboe}/AudioClock.h | 1 - .../src/jni/oboe/include/oboe/AudioStream.h | 232 +- .../jni/oboe/include/oboe/AudioStreamBase.h | 169 +- .../oboe/include/oboe/AudioStreamBuilder.h | 288 +- .../oboe/include/oboe/AudioStreamCallback.h | 105 +- .../src/jni/oboe/include/oboe/Definitions.h | 384 +- .../{src/fifo => include/oboe}/FifoBuffer.h | 73 +- .../oboe}/FifoControllerBase.h | 47 +- .../jni/oboe/include/oboe/FullDuplexStream.h | 356 + .../src/jni/oboe/include/oboe/LatencyTuner.h | 36 +- .../love/src/jni/oboe/include/oboe/Oboe.h | 4 + .../jni/oboe/include/oboe/OboeExtensions.h | 64 + .../oboe/include/oboe/StabilizedCallback.h | 2 +- .../src/jni/oboe/include/oboe/Utilities.h | 12 + .../love/src/jni/oboe/include/oboe/Version.h | 2 +- .../love/src/jni/oboe/prefab/oboe-VERSION.pom | 2 +- .../prefab/oboe-VERSION/AndroidManifest.xml | 6 +- .../android/love/src/jni/oboe/prefab_build.sh | 0 .../src/jni/oboe/samples/LiveEffect/README.md | 19 +- .../jni/oboe/samples/LiveEffect/build.gradle | 11 +- .../LiveEffect/src/main/AndroidManifest.xml | 31 +- .../LiveEffect/src/main/cpp/CMakeLists.txt | 2 +- .../LiveEffect/src/main/cpp/FullDuplexPass.h | 39 +- .../src/main/cpp/FullDuplexStream.cpp | 128 - .../src/main/cpp/FullDuplexStream.h | 107 - .../src/main/cpp/LiveEffectEngine.cpp | 100 +- .../src/main/cpp/LiveEffectEngine.h | 47 +- .../LiveEffect/src/main/cpp/jni_bridge.cpp | 25 +- .../DuplexStreamForegroundService.java | 92 + .../samples}/liveEffect/LiveEffectEngine.java | 6 +- .../samples}/liveEffect/MainActivity.java | 84 +- .../src/main/res/drawable/balance_seekbar.xml | 17 - .../src/main/res/layout-v21/activity_main.xml | 93 +- .../src/main/res/layout/activity_main.xml | 94 +- .../LiveEffect/src/main/res/values/colors.xml | 2 +- .../src/jni/oboe/samples/MegaDrone/README.md | 6 +- .../jni/oboe/samples/MegaDrone/build.gradle | 13 +- .../MegaDrone/src/main/AndroidManifest.xml | 10 +- .../MegaDrone/src/main/cpp/CMakeLists.txt | 3 +- .../src/main/cpp/MegaDroneEngine.cpp | 75 +- .../MegaDrone/src/main/cpp/MegaDroneEngine.h | 19 +- .../MegaDrone/src/main/cpp/native-lib.cpp | 18 +- .../oboe/samples}/megadrone/MainActivity.java | 4 +- .../love/src/jni/oboe/samples/README.md | 15 +- .../oboe/samples/RhythmGame/CMakeLists.txt | 6 +- .../src/jni/oboe/samples/RhythmGame/README.md | 10 +- .../jni/oboe/samples/RhythmGame/build.gradle | 19 +- .../RhythmGame/src/main/AndroidManifest.xml | 8 +- .../samples/RhythmGame/src/main/cpp/Game.cpp | 58 +- .../samples/RhythmGame/src/main/cpp/Game.h | 9 +- .../RhythmGame/src/main/cpp/GameConstants.h | 2 +- .../src/main/cpp/audio/AAssetDataSource.cpp | 2 +- .../src/main/cpp/audio/FFMpegExtractor.cpp | 94 +- .../RhythmGame/src/main/cpp/audio/Player.cpp | 2 +- .../RhythmGame/src/main/cpp/native-lib.cpp | 18 +- .../src/main/cpp/utils/UtilityFunctions.cpp | 2 +- .../rhythmgame/GameSurfaceView.java | 2 +- .../rhythmgame/MainActivity.java | 10 +- .../rhythmgame/RendererWrapper.java | 2 +- .../src/main/res/layout/activity_main.xml | 20 +- .../samples/RhythmGame/test/CMakeLists.txt | 2 +- .../oboe/samples/RhythmGame/test/run_tests.sh | 6 +- .../src/jni/oboe/samples/SoundBoard/README.md | 46 + .../jni/oboe/samples/SoundBoard/build.gradle | 54 + .../samples/SoundBoard/proguard-rules.pro | 21 + .../samples/SoundBoard/soundboard_image.png | Bin 0 -> 39429 bytes .../SoundBoard/src/main/AndroidManifest.xml | 21 + .../SoundBoard/src/main/cpp/CMakeLists.txt | 29 + .../src/main/cpp/SoundBoardEngine.cpp | 130 + .../src/main/cpp/SoundBoardEngine.h | 65 + .../samples/SoundBoard/src/main/cpp/Synth.h | 94 + .../SoundBoard/src/main/cpp/native-lib.cpp | 91 + .../src/main/ic_launcher-playstore.png | Bin 0 -> 4673 bytes .../oboe/samples/soundboard/MainActivity.kt | 132 + .../oboe/samples/soundboard/MusicTileView.kt | 164 + .../oboe/samples/soundboard/NoteListener.kt | 31 + .../drawable-v24/ic_launcher_foreground.xml | 34 + .../res/drawable/ic_launcher_background.xml | 74 + .../src/main/res/layout/activity_main.xml | 9 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + .../mipmap-hdpi/ic_launcher_foreground.png | Bin 0 -> 853 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 0 -> 576 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 0 -> 1200 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 0 -> 1783 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 0 -> 2993 bytes .../SoundBoard/src/main/res/values/colors.xml | 6 + .../src/main/res/values/strings.xml | 3 + .../SoundBoard/src/main/res/values/styles.xml | 11 + .../oboe/samples/audio-device/build.gradle | 9 +- .../audio-device/src/main/AndroidManifest.xml | 2 +- .../audio_device/AudioDeviceAdapter.java | 2 +- .../AudioDeviceInfoConverter.java | 2 +- .../audio_device/AudioDeviceListEntry.java | 2 +- .../audio_device/AudioDeviceSpinner.java | 2 +- .../love/src/jni/oboe/samples/build.gradle | 15 +- .../jni/oboe/samples/drumthumper/README.md | 30 +- .../jni/oboe/samples/drumthumper/build.gradle | 37 +- .../drumthumper/drumthumper-screenshot.png | Bin 0 -> 118282 bytes .../playstore/DrumSetFeatureImage.png | Bin 0 -> 83513 bytes .../drumthumper/playstore/ScreenShotPhone.png | Bin 0 -> 96949 bytes .../playstore/ScreenShotWithMix.png | Bin 0 -> 60947 bytes .../samples/drumthumper/playstore/drumset.jpg | Bin 0 -> 33988 bytes .../drumthumper/src/main/AndroidManifest.xml | 8 +- .../drumthumper/src/main/cpp/CMakeLists.txt | 3 +- .../src/main/cpp/DrumPlayerJNI.cpp | 46 +- .../plausibleaudio}/drumthumper/DrumPlayer.kt | 38 +- .../drumthumper/DrumThumperActivity.kt | 82 +- .../plausibleaudio}/drumthumper/TriggerPad.kt | 123 +- .../res/layout-land/drumthumper_activity.xml | 26 +- .../main/res/layout/drumthumper_activity.xml | 26 +- .../src/jni/oboe/samples/gradle.properties | 3 + .../gradle/wrapper/gradle-wrapper.properties | 4 +- .../android/love/src/jni/oboe/samples/gradlew | 0 .../src/jni/oboe/samples/hello-oboe/README.md | 2 +- .../jni/oboe/samples/hello-oboe/build.gradle | 13 +- .../hello-oboe/src/main/AndroidManifest.xml | 8 +- .../hello-oboe/src/main/cpp/CMakeLists.txt | 1 + .../src/main/cpp/HelloOboeEngine.cpp | 195 +- .../hello-oboe/src/main/cpp/HelloOboeEngine.h | 44 +- .../src/main/cpp/LatencyTuningCallback.cpp | 11 +- .../src/main/cpp/LatencyTuningCallback.h | 11 +- .../hello-oboe/src/main/cpp/jni_bridge.cpp | 143 +- .../samples/hellooboe/BackgroundRunner.java | 56 + .../samples}/hellooboe/MainActivity.java | 230 +- .../samples/hellooboe/PlaybackEngine.java | 52 + .../sample/oboe/hellooboe/PlaybackEngine.java | 99 - .../src/main/res/layout/activity_main.xml | 2 +- .../src/jni/oboe/samples/iolib/build.gradle | 11 +- .../iolib/src/main/AndroidManifest.xml | 3 +- .../samples/iolib/src/main/cpp/CMakeLists.txt | 1 + .../main/cpp/player/OneShotSampleSource.cpp | 39 +- .../src/main/cpp/player/SampleBuffer.cpp | 76 +- .../iolib/src/main/cpp/player/SampleBuffer.h | 4 +- .../iolib/src/main/cpp/player/SampleSource.h | 8 +- .../src/main/cpp/player/SimpleMultiPlayer.cpp | 88 +- .../src/main/cpp/player/SimpleMultiPlayer.h | 58 +- .../jni/oboe/samples/minimaloboe/.gitignore | 1 + .../jni/oboe/samples/minimaloboe/README.md | 19 + .../jni/oboe/samples/minimaloboe/build.gradle | 77 + .../minimaloboe/minimaloboe-screenshot.png | Bin 0 -> 98980 bytes .../samples/minimaloboe/proguard-rules.pro | 21 + .../minimaloboe/src/main/AndroidManifest.xml | 24 + .../minimaloboe/src/main/cpp/CMakeLists.txt | 53 + .../src/main/cpp/MinimalOboeJNI.cpp | 66 + .../src/main/cpp/SimpleNoiseMaker.cpp | 92 + .../src/main/cpp/SimpleNoiseMaker.h | 71 + .../com/example/minimaloboe/AudioPlayer.kt | 87 + .../com/example/minimaloboe/MainActivity.kt | 116 + .../com/example/minimaloboe/ui/theme/Color.kt | 8 + .../com/example/minimaloboe/ui/theme/Shape.kt | 11 + .../com/example/minimaloboe/ui/theme/Theme.kt | 44 + .../com/example/minimaloboe/ui/theme/Type.kt | 21 + .../drawable-v24/ic_launcher_foreground.xml | 30 + .../res/drawable/ic_launcher_background.xml | 170 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + .../src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 1404 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 2898 bytes .../src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 982 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 1772 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 1900 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 3918 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 2884 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 5914 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 3844 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 7778 bytes .../src/main/res/values/colors.xml | 10 + .../src/main/res/values/strings.xml | 3 + .../src/main/res/values/themes.xml | 7 + .../jni/oboe/samples/parselib/build.gradle | 11 +- .../parselib/src/main/AndroidManifest.xml | 3 +- .../parselib/src/main/cpp/wav/AudioEncoding.h | 2 + .../src/main/cpp/wav/WavStreamReader.cpp | 246 +- .../src/main/cpp/wav/WavStreamReader.h | 24 +- .../love/src/jni/oboe/samples/settings.gradle | 2 + ...StreamCallback.h => DefaultDataCallback.h} | 43 +- .../samples/shared/DefaultErrorCallback.h | 56 + .../love/src/jni/oboe/samples/shared/Mixer.h | 31 +- .../src/jni/oboe/samples/shared/Oscillator.h | 2 +- .../src/jni/oboe/samples/shared/SynthSound.h | 109 + .../oboe/samples/shared/TappableAudioSource.h | 2 +- .../jni/oboe/src/aaudio/AAudioExtensions.h | 180 + .../src/jni/oboe/src/aaudio/AAudioLoader.cpp | 176 +- .../src/jni/oboe/src/aaudio/AAudioLoader.h | 82 +- .../jni/oboe/src/aaudio/AudioStreamAAudio.cpp | 417 +- .../jni/oboe/src/aaudio/AudioStreamAAudio.h | 47 +- .../src/jni/oboe/src/common/AdpfWrapper.cpp | 141 + .../src/jni/oboe/src/common/AdpfWrapper.h | 89 + .../jni/oboe/src/common/AudioSourceCaller.cpp | 2 +- .../src/jni/oboe/src/common/AudioStream.cpp | 57 +- .../oboe/src/common/AudioStreamBuilder.cpp | 91 +- .../src/common/DataConversionFlowGraph.cpp | 123 +- .../oboe/src/common/DataConversionFlowGraph.h | 8 +- .../jni/oboe/src/common/FilterAudioStream.cpp | 16 +- .../jni/oboe/src/common/FilterAudioStream.h | 71 +- .../src/jni/oboe/src/common/LatencyTuner.cpp | 20 +- .../jni/oboe/src/common/OboeExtensions.cpp | 36 + .../src/jni/oboe/src/common/QuirksManager.cpp | 237 +- .../src/jni/oboe/src/common/QuirksManager.h | 24 + .../src/jni/oboe/src/common/SourceI16Caller.h | 3 +- .../jni/oboe/src/common/SourceI24Caller.cpp | 56 + .../src/jni/oboe/src/common/SourceI24Caller.h | 53 + .../jni/oboe/src/common/SourceI32Caller.cpp | 47 + .../src/jni/oboe/src/common/SourceI32Caller.h | 53 + .../oboe/src/common/StabilizedCallback.cpp | 6 +- .../love/src/jni/oboe/src/common/Trace.cpp | 2 +- .../love/src/jni/oboe/src/common/Trace.h | 2 +- .../src/jni/oboe/src/common/Utilities.cpp | 44 +- .../love/src/jni/oboe/src/fifo/FifoBuffer.cpp | 12 +- .../src/jni/oboe/src/fifo/FifoController.cpp | 5 +- .../src/jni/oboe/src/fifo/FifoController.h | 5 +- .../jni/oboe/src/fifo/FifoControllerBase.cpp | 9 +- .../oboe/src/fifo/FifoControllerIndirect.cpp | 1 + .../oboe/src/fifo/FifoControllerIndirect.h | 4 +- .../src/flowgraph/ChannelCountConverter.cpp | 52 + .../src/flowgraph/ChannelCountConverter.h | 52 + .../jni/oboe/src/flowgraph/ClipToRange.cpp | 2 +- .../src/jni/oboe/src/flowgraph/ClipToRange.h | 4 +- .../jni/oboe/src/flowgraph/FlowGraphNode.cpp | 27 +- .../jni/oboe/src/flowgraph/FlowGraphNode.h | 58 +- .../oboe/src/flowgraph/FlowgraphUtilities.h | 70 + .../src/jni/oboe/src/flowgraph/Limiter.cpp | 67 + .../love/src/jni/oboe/src/flowgraph/Limiter.h | 64 + .../src/flowgraph/ManyToMultiConverter.cpp | 2 +- .../oboe/src/flowgraph/ManyToMultiConverter.h | 6 +- .../src/jni/oboe/src/flowgraph/MonoBlend.cpp | 46 + .../src/jni/oboe/src/flowgraph/MonoBlend.h | 48 + .../src/flowgraph/MonoToMultiConverter.cpp | 8 +- .../oboe/src/flowgraph/MonoToMultiConverter.h | 10 +- .../src/flowgraph/MultiToManyConverter.cpp | 47 + .../oboe/src/flowgraph/MultiToManyConverter.h | 49 + .../src/flowgraph/MultiToMonoConverter.cpp | 41 + .../oboe/src/flowgraph/MultiToMonoConverter.h | 49 + .../src/jni/oboe/src/flowgraph/RampLinear.cpp | 6 +- .../src/jni/oboe/src/flowgraph/RampLinear.h | 4 +- .../src/flowgraph/SampleRateConverter.cpp | 18 +- .../oboe/src/flowgraph/SampleRateConverter.h | 25 +- .../src/jni/oboe/src/flowgraph/SinkFloat.cpp | 12 +- .../src/jni/oboe/src/flowgraph/SinkFloat.h | 7 +- .../src/jni/oboe/src/flowgraph/SinkI16.cpp | 7 +- .../love/src/jni/oboe/src/flowgraph/SinkI16.h | 6 +- .../src/jni/oboe/src/flowgraph/SinkI24.cpp | 7 +- .../love/src/jni/oboe/src/flowgraph/SinkI24.h | 6 +- .../src/jni/oboe/src/flowgraph/SinkI32.cpp | 55 + .../love/src/jni/oboe/src/flowgraph/SinkI32.h | 40 + .../src/jni/oboe/src/flowgraph/SinkI8_24.cpp | 55 + .../src/jni/oboe/src/flowgraph/SinkI8_24.h | 40 + .../jni/oboe/src/flowgraph/SourceFloat.cpp | 11 +- .../src/jni/oboe/src/flowgraph/SourceFloat.h | 5 +- .../src/jni/oboe/src/flowgraph/SourceI16.cpp | 4 +- .../src/jni/oboe/src/flowgraph/SourceI16.h | 4 +- .../src/jni/oboe/src/flowgraph/SourceI24.cpp | 10 +- .../src/jni/oboe/src/flowgraph/SourceI24.h | 4 +- .../src/jni/oboe/src/flowgraph/SourceI32.cpp | 54 + .../src/jni/oboe/src/flowgraph/SourceI32.h | 42 + .../jni/oboe/src/flowgraph/SourceI8_24.cpp | 54 + .../src/jni/oboe/src/flowgraph/SourceI8_24.h | 42 + .../resampler/HyperbolicCosineWindow.h | 7 +- .../src/flowgraph/resampler/IntegerRatio.cpp | 2 +- .../src/flowgraph/resampler/IntegerRatio.h | 12 +- .../src/flowgraph/resampler/KaiserWindow.h | 7 +- .../flowgraph/resampler/LinearResampler.cpp | 2 +- .../src/flowgraph/resampler/LinearResampler.h | 17 +- .../resampler/MultiChannelResampler.cpp | 22 +- .../resampler/MultiChannelResampler.h | 20 +- .../resampler/PolyphaseResampler.cpp | 11 +- .../flowgraph/resampler/PolyphaseResampler.h | 18 +- .../resampler/PolyphaseResamplerMono.cpp | 3 +- .../resampler/PolyphaseResamplerMono.h | 14 +- .../resampler/PolyphaseResamplerStereo.cpp | 3 +- .../resampler/PolyphaseResamplerStereo.h | 14 +- .../oboe/src/flowgraph/resampler/README.md | 28 +- .../resampler/ResamplerDefinitions.h | 27 + .../src/flowgraph/resampler/SincResampler.cpp | 53 +- .../src/flowgraph/resampler/SincResampler.h | 15 +- .../resampler/SincResamplerStereo.cpp | 14 +- .../flowgraph/resampler/SincResamplerStereo.h | 15 +- .../src/opensles/AudioInputStreamOpenSLES.cpp | 75 +- .../src/opensles/AudioInputStreamOpenSLES.h | 5 +- .../opensles/AudioOutputStreamOpenSLES.cpp | 109 +- .../src/opensles/AudioOutputStreamOpenSLES.h | 7 +- .../oboe/src/opensles/AudioStreamBuffered.cpp | 61 +- .../oboe/src/opensles/AudioStreamBuffered.h | 12 +- .../oboe/src/opensles/AudioStreamOpenSLES.cpp | 251 +- .../oboe/src/opensles/AudioStreamOpenSLES.h | 47 +- .../jni/oboe/src/opensles/EngineOpenSLES.cpp | 119 +- .../jni/oboe/src/opensles/EngineOpenSLES.h | 42 + .../oboe/src/opensles/OpenSLESUtilities.cpp | 41 +- .../oboe/src/opensles/OutputMixerOpenSLES.h | 3 +- .../love/src/jni/oboe/tests/CMakeLists.txt | 28 +- mobile/android/love/src/jni/oboe/tests/README | 1 - .../android/love/src/jni/oboe/tests/README.md | 30 +- .../tests/UnitTestRunner/app/build.gradle | 14 +- .../UnitTestRunner/app/src/main/.gitignore | 2 + .../app/src/main/AndroidManifest.xml | 7 +- .../tests/unittestrunner/MainActivity.java | 124 +- .../app/src/main/res/layout/activity_main.xml | 2 +- .../app/src/main/res/values/strings.xml | 2 +- .../oboe/tests/UnitTestRunner/build.gradle | 22 +- .../tests/UnitTestRunner/gradle.properties | 3 + .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../src/jni/oboe/tests/UnitTestRunner/gradlew | 0 .../love/src/jni/oboe/tests/run_tests.sh | 55 +- .../love/src/jni/oboe/tests/testAAudio.cpp | 74 + .../src/jni/oboe/tests/testAudioClock.cpp | 92 + .../love/src/jni/oboe/tests/testFlowgraph.cpp | 186 +- .../jni/oboe/tests/testFullDuplexStream.cpp | 206 + .../love/src/jni/oboe/tests/testResampler.cpp | 225 + .../src/jni/oboe/tests/testReturnStop.cpp | 133 + .../jni/oboe/tests/testReturnStopDeadlock.cpp | 161 + .../oboe/tests/testStreamClosedMethods.cpp | 257 +- .../oboe/tests/testStreamFramesProcessed.cpp | 99 + .../src/jni/oboe/tests/testStreamOpen.cpp | 595 +- .../src/jni/oboe/tests/testStreamStates.cpp | 83 +- .../src/jni/oboe/tests/testStreamStop.cpp | 130 + .../jni/oboe/tests/testStreamWaitState.cpp | 137 +- .../love/src/jni/oboe/tests/testUtilities.cpp | 5 +- .../src/jni/oboe/tests/testXRunBehaviour.cpp | 52 +- .../jni/openal-soft/.github/workflows/ci.yml | 241 + .../openal-soft/.github/workflows/utils.yml | 85 + .../love/src/jni/openal-soft/.gitignore | 1 + .../love/src/jni/openal-soft/Android.mk | 181 +- .../love/src/jni/openal-soft/BSD-3Clause | 1 + .../love/src/jni/openal-soft/CMakeLists.txt | 1498 +- .../love/src/jni/openal-soft/ChangeLog | 253 + .../love/src/jni/openal-soft/LICENSE-pffft | 38 + .../src/jni/openal-soft/OpenALConfig.cmake.in | 9 + .../love/src/jni/openal-soft/README.md | 66 +- .../src/jni/openal-soft/XCompile-Android.txt | 5 + .../src/jni/openal-soft/al/auxeffectslot.cpp | 1662 +- .../src/jni/openal-soft/al/auxeffectslot.h | 413 +- .../love/src/jni/openal-soft/al/buffer.cpp | 2499 +- .../love/src/jni/openal-soft/al/buffer.h | 86 +- .../love/src/jni/openal-soft/al/debug.cpp | 608 + .../love/src/jni/openal-soft/al/debug.h | 70 + .../love/src/jni/openal-soft/al/direct_defs.h | 127 + .../love/src/jni/openal-soft/al/eax/api.cpp | 1621 + .../love/src/jni/openal-soft/al/eax/api.h | 1522 + .../love/src/jni/openal-soft/al/eax/call.cpp | 224 + .../love/src/jni/openal-soft/al/eax/call.h | 97 + .../love/src/jni/openal-soft/al/eax/effect.h | 470 + .../src/jni/openal-soft/al/eax/exception.cpp | 32 + .../src/jni/openal-soft/al/eax/exception.h | 23 + .../jni/openal-soft/al/eax/fx_slot_index.cpp | 71 + .../jni/openal-soft/al/eax/fx_slot_index.h | 40 + .../src/jni/openal-soft/al/eax/fx_slots.cpp | 75 + .../src/jni/openal-soft/al/eax/fx_slots.h | 43 + .../love/src/jni/openal-soft/al/eax/globals.h | 6 + .../love/src/jni/openal-soft/al/eax/utils.cpp | 26 + .../love/src/jni/openal-soft/al/eax/utils.h | 95 + .../love/src/jni/openal-soft/al/eax/x_ram.h | 30 + .../love/src/jni/openal-soft/al/effect.cpp | 770 +- .../love/src/jni/openal-soft/al/effect.h | 58 +- .../jni/openal-soft/al/effects/autowah.cpp | 240 + .../src/jni/openal-soft/al/effects/chorus.cpp | 646 + .../jni/openal-soft/al/effects/compressor.cpp | 152 + .../openal-soft/al/effects/convolution.cpp | 117 + .../jni/openal-soft/al/effects/dedicated.cpp | 122 + .../jni/openal-soft/al/effects/distortion.cpp | 257 + .../src/jni/openal-soft/al/effects/echo.cpp | 254 + .../jni/openal-soft/al/effects/effects.cpp | 3 + .../src/jni/openal-soft/al/effects/effects.h | 66 + .../jni/openal-soft/al/effects/equalizer.cpp | 377 + .../jni/openal-soft/al/effects/fshifter.cpp | 267 + .../jni/openal-soft/al/effects/modulator.cpp | 265 + .../src/jni/openal-soft/al/effects/null.cpp | 143 + .../jni/openal-soft/al/effects/pshifter.cpp | 182 + .../src/jni/openal-soft/al/effects/reverb.cpp | 1238 + .../jni/openal-soft/al/effects/vmorpher.cpp | 456 + .../love/src/jni/openal-soft/al/error.cpp | 117 +- .../love/src/jni/openal-soft/al/error.h | 27 + .../love/src/jni/openal-soft/al/event.cpp | 240 +- .../love/src/jni/openal-soft/al/event.h | 49 +- .../love/src/jni/openal-soft/al/extension.cpp | 63 +- .../love/src/jni/openal-soft/al/filter.cpp | 969 +- .../love/src/jni/openal-soft/al/filter.h | 80 +- .../love/src/jni/openal-soft/al/listener.cpp | 535 +- .../love/src/jni/openal-soft/al/listener.h | 46 +- .../love/src/jni/openal-soft/al/source.cpp | 6197 +- .../love/src/jni/openal-soft/al/source.h | 1014 +- .../love/src/jni/openal-soft/al/state.cpp | 1139 +- .../love/src/jni/openal-soft/alc/alc.cpp | 4282 +- .../love/src/jni/openal-soft/alc/alcmain.h | 383 - .../love/src/jni/openal-soft/alc/alconfig.cpp | 486 +- .../love/src/jni/openal-soft/alc/alconfig.h | 23 +- .../love/src/jni/openal-soft/alc/alcontext.h | 258 - .../love/src/jni/openal-soft/alc/alu.cpp | 2184 +- .../love/src/jni/openal-soft/alc/alu.h | 153 +- .../love/src/jni/openal-soft/alc/ambdec.cpp | 434 - .../love/src/jni/openal-soft/alc/ambdec.h | 48 - .../love/src/jni/openal-soft/alc/ambidefs.h | 132 - .../src/jni/openal-soft/alc/backends/alsa.cpp | 559 +- .../src/jni/openal-soft/alc/backends/alsa.h | 12 +- .../src/jni/openal-soft/alc/backends/base.cpp | 203 +- .../src/jni/openal-soft/alc/backends/base.h | 104 +- .../openal-soft/alc/backends/coreaudio.cpp | 993 +- .../jni/openal-soft/alc/backends/coreaudio.h | 14 +- .../jni/openal-soft/alc/backends/dsound.cpp | 399 +- .../src/jni/openal-soft/alc/backends/dsound.h | 12 +- .../src/jni/openal-soft/alc/backends/jack.cpp | 591 +- .../src/jni/openal-soft/alc/backends/jack.h | 12 +- .../jni/openal-soft/alc/backends/loopback.cpp | 21 +- .../jni/openal-soft/alc/backends/loopback.h | 12 +- .../src/jni/openal-soft/alc/backends/null.cpp | 52 +- .../src/jni/openal-soft/alc/backends/null.h | 12 +- .../src/jni/openal-soft/alc/backends/oboe.cpp | 272 +- .../src/jni/openal-soft/alc/backends/oboe.h | 12 +- .../jni/openal-soft/alc/backends/opensl.cpp | 468 +- .../src/jni/openal-soft/alc/backends/opensl.h | 12 +- .../src/jni/openal-soft/alc/backends/oss.cpp | 353 +- .../src/jni/openal-soft/alc/backends/oss.h | 12 +- .../jni/openal-soft/alc/backends/otherio.cpp | 700 + .../jni/openal-soft/alc/backends/otherio.h | 21 + .../jni/openal-soft/alc/backends/pipewire.cpp | 2313 + .../jni/openal-soft/alc/backends/pipewire.h | 27 + .../openal-soft/alc/backends/portaudio.cpp | 421 +- .../jni/openal-soft/alc/backends/portaudio.h | 12 +- .../openal-soft/alc/backends/pulseaudio.cpp | 1181 +- .../jni/openal-soft/alc/backends/pulseaudio.h | 20 +- .../src/jni/openal-soft/alc/backends/sdl2.cpp | 205 +- .../src/jni/openal-soft/alc/backends/sdl2.h | 12 +- .../jni/openal-soft/alc/backends/sndio.cpp | 511 +- .../src/jni/openal-soft/alc/backends/sndio.h | 12 +- .../jni/openal-soft/alc/backends/solaris.cpp | 179 +- .../jni/openal-soft/alc/backends/solaris.h | 12 +- .../jni/openal-soft/alc/backends/wasapi.cpp | 2407 +- .../src/jni/openal-soft/alc/backends/wasapi.h | 14 +- .../src/jni/openal-soft/alc/backends/wave.cpp | 317 +- .../src/jni/openal-soft/alc/backends/wave.h | 12 +- .../jni/openal-soft/alc/backends/winmm.cpp | 275 +- .../src/jni/openal-soft/alc/backends/winmm.h | 12 +- .../src/jni/openal-soft/alc/bformatdec.cpp | 301 - .../love/src/jni/openal-soft/alc/bformatdec.h | 77 - .../love/src/jni/openal-soft/alc/bs2b.cpp | 183 - .../love/src/jni/openal-soft/alc/bs2b.h | 89 - .../love/src/jni/openal-soft/alc/bsinc_defs.h | 16 - .../src/jni/openal-soft/alc/bsinc_tables.cpp | 289 - .../src/jni/openal-soft/alc/bsinc_tables.h | 17 - .../jni/openal-soft/alc/buffer_storage.cpp | 37 - .../src/jni/openal-soft/alc/buffer_storage.h | 73 - .../love/src/jni/openal-soft/alc/bufferline.h | 15 - .../love/src/jni/openal-soft/alc/compat.h | 9 - .../love/src/jni/openal-soft/alc/context.cpp | 1077 + .../love/src/jni/openal-soft/alc/context.h | 563 + .../src/jni/openal-soft/alc/converter.cpp | 372 - .../love/src/jni/openal-soft/alc/converter.h | 62 - .../love/src/jni/openal-soft/alc/cpu_caps.cpp | 165 - .../love/src/jni/openal-soft/alc/cpu_caps.h | 16 - .../love/src/jni/openal-soft/alc/devformat.h | 103 - .../love/src/jni/openal-soft/alc/device.cpp | 97 + .../love/src/jni/openal-soft/alc/device.h | 129 + .../jni/openal-soft/alc/effects/autowah.cpp | 274 +- .../src/jni/openal-soft/alc/effects/base.h | 212 +- .../jni/openal-soft/alc/effects/chorus.cpp | 572 +- .../openal-soft/alc/effects/compressor.cpp | 277 +- .../openal-soft/alc/effects/convolution.cpp | 771 +- .../jni/openal-soft/alc/effects/dedicated.cpp | 150 +- .../openal-soft/alc/effects/distortion.cpp | 196 +- .../src/jni/openal-soft/alc/effects/echo.cpp | 217 +- .../jni/openal-soft/alc/effects/equalizer.cpp | 283 +- .../jni/openal-soft/alc/effects/fshifter.cpp | 326 +- .../jni/openal-soft/alc/effects/modulator.cpp | 330 +- .../src/jni/openal-soft/alc/effects/null.cpp | 125 +- .../jni/openal-soft/alc/effects/pshifter.cpp | 399 +- .../jni/openal-soft/alc/effects/reverb.cpp | 2359 +- .../jni/openal-soft/alc/effects/vmorpher.cpp | 429 +- .../love/src/jni/openal-soft/alc/events.cpp | 95 + .../love/src/jni/openal-soft/alc/events.h | 50 + .../src/jni/openal-soft/alc/export_list.h | 925 + .../jni/openal-soft/alc/filters/splitter.h | 36 - .../love/src/jni/openal-soft/alc/fmt_traits.h | 81 - .../love/src/jni/openal-soft/alc/fpu_ctrl.cpp | 54 - .../love/src/jni/openal-soft/alc/fpu_ctrl.h | 25 - .../src/jni/openal-soft/alc/front_stablizer.h | 36 - .../love/src/jni/openal-soft/alc/helpers.cpp | 512 - .../love/src/jni/openal-soft/alc/hrtf.cpp | 1481 - .../love/src/jni/openal-soft/alc/hrtf.h | 120 - .../love/src/jni/openal-soft/alc/inprogext.h | 121 +- .../love/src/jni/openal-soft/alc/logging.h | 47 - .../src/jni/openal-soft/alc/mastering.cpp | 443 - .../love/src/jni/openal-soft/alc/mixer/defs.h | 48 - .../src/jni/openal-soft/alc/mixer/hrtfbase.h | 160 - .../src/jni/openal-soft/alc/mixer/mixer_c.cpp | 196 - .../jni/openal-soft/alc/mixer/mixer_neon.cpp | 293 - .../jni/openal-soft/alc/mixer/mixer_sse.cpp | 266 - .../jni/openal-soft/alc/mixer/mixer_sse2.cpp | 85 - .../jni/openal-soft/alc/mixer/mixer_sse41.cpp | 90 - .../love/src/jni/openal-soft/alc/panning.cpp | 1779 +- .../src/jni/openal-soft/alc/ringbuffer.cpp | 236 - .../love/src/jni/openal-soft/alc/ringbuffer.h | 111 - .../src/jni/openal-soft/alc/uhjfilter.cpp | 208 - .../love/src/jni/openal-soft/alc/uhjfilter.h | 39 - .../love/src/jni/openal-soft/alc/voice.cpp | 803 - .../love/src/jni/openal-soft/alc/voice.h | 264 - .../love/src/jni/openal-soft/alsoftrc.sample | 303 +- .../love/src/jni/openal-soft/appveyor.yml | 12 +- .../jni/openal-soft/cmake/FindDSound.cmake | 41 - .../jni/openal-soft/cmake/FindFFmpeg.cmake | 18 +- .../src/jni/openal-soft/cmake/FindJACK.cmake | 2 +- .../jni/openal-soft/cmake/FindOpenSL.cmake | 17 +- .../openal-soft/cmake/FindPulseAudio.cmake | 9 - .../src/jni/openal-soft/cmake/FindSDL2.cmake | 191 - .../src/jni/openal-soft/cmake/FindSndIO.cmake | 31 + .../jni/openal-soft/cmake/FindSoundIO.cmake | 32 - .../openal-soft/cmake/FindWindowsSDK.cmake | 632 - .../jni/openal-soft/cmake/bin2h.script.cmake | 5 +- .../src/jni/openal-soft/common/alassert.cpp | 44 + .../src/jni/openal-soft/common/alassert.h | 24 + .../love/src/jni/openal-soft/common/albit.h | 181 + .../love/src/jni/openal-soft/common/albyte.h | 111 - .../src/jni/openal-soft/common/alcomplex.cpp | 208 +- .../src/jni/openal-soft/common/alcomplex.h | 2 +- .../love/src/jni/openal-soft/common/alexcpt.h | 35 - .../src/jni/openal-soft/common/alfstream.cpp | 147 - .../src/jni/openal-soft/common/alfstream.h | 70 - .../src/jni/openal-soft/common/almalloc.cpp | 64 - .../src/jni/openal-soft/common/almalloc.h | 255 +- .../src/jni/openal-soft/common/alnumbers.h | 32 + .../src/jni/openal-soft/common/alnumeric.h | 295 +- .../src/jni/openal-soft/common/aloptional.h | 149 - .../common/{threads.cpp => alsem.cpp} | 63 +- .../love/src/jni/openal-soft/common/alsem.h | 43 + .../love/src/jni/openal-soft/common/alspan.h | 505 +- .../src/jni/openal-soft/common/alstring.cpp | 57 +- .../src/jni/openal-soft/common/alstring.h | 42 +- .../jni/openal-soft/common/althrd_setname.cpp | 77 + .../jni/openal-soft/common/althrd_setname.h | 6 + .../src/jni/openal-soft/common/althreads.h | 143 + .../src/jni/openal-soft/common/altraits.h | 14 + .../love/src/jni/openal-soft/common/atomic.h | 87 +- .../love/src/jni/openal-soft/common/comptr.h | 113 + .../src/jni/openal-soft/common/dynload.cpp | 4 +- .../src/jni/openal-soft/common/endiantest.h | 14 - .../src/jni/openal-soft/common/flexarray.h | 139 + .../jni/openal-soft/common/intrusive_ptr.h | 65 +- .../src/jni/openal-soft/common/math_defs.h | 28 - .../src/jni/openal-soft/common/opthelpers.h | 113 +- .../love/src/jni/openal-soft/common/pffft.cpp | 2308 + .../love/src/jni/openal-soft/common/pffft.h | 212 + .../jni/openal-soft/common/phase_shifter.h | 214 + .../common/polyphase_resampler.cpp | 143 +- .../openal-soft/common/polyphase_resampler.h | 12 +- .../src/jni/openal-soft/common/ringbuffer.cpp | 173 + .../src/jni/openal-soft/common/ringbuffer.h | 136 + .../src/jni/openal-soft/common/strutils.cpp | 41 +- .../src/jni/openal-soft/common/strutils.h | 14 +- .../love/src/jni/openal-soft/common/threads.h | 48 - .../love/src/jni/openal-soft/common/vecmat.h | 99 +- .../love/src/jni/openal-soft/common/vector.h | 3 +- .../jni/openal-soft/common/win_main_utf8.h | 13 +- .../android/love/src/jni/openal-soft/config.h | 132 +- .../love/src/jni/openal-soft/config.h.in | 98 +- .../src/jni/openal-soft/config_backends.h | 42 + .../src/jni/openal-soft/config_backends.h.in | 35 + .../love/src/jni/openal-soft/config_simd.h | 25 + .../love/src/jni/openal-soft/config_simd.h.in | 10 + .../jni/openal-soft/configs/HRTF/alsoft.ini | 4 + .../love/src/jni/openal-soft/core/ambdec.cpp | 310 + .../love/src/jni/openal-soft/core/ambdec.h | 56 + .../src/jni/openal-soft/core/ambidefs.cpp | 311 + .../love/src/jni/openal-soft/core/ambidefs.h | 211 + .../src/jni/openal-soft/core/async_event.h | 65 + .../src/jni/openal-soft/core/bformatdec.cpp | 179 + .../src/jni/openal-soft/core/bformatdec.h | 62 + .../love/src/jni/openal-soft/core/bs2b.cpp | 189 + .../love/src/jni/openal-soft/core/bs2b.h | 89 + .../src/jni/openal-soft/core/bsinc_defs.h | 12 + .../src/jni/openal-soft/core/bsinc_tables.cpp | 303 + .../src/jni/openal-soft/core/bsinc_tables.h | 20 + .../jni/openal-soft/core/buffer_storage.cpp | 5 + .../src/jni/openal-soft/core/buffer_storage.h | 71 + .../src/jni/openal-soft/core/bufferline.h | 17 + .../love/src/jni/openal-soft/core/context.cpp | 180 + .../love/src/jni/openal-soft/core/context.h | 185 + .../src/jni/openal-soft/core/converter.cpp | 457 + .../love/src/jni/openal-soft/core/converter.h | 69 + .../src/jni/openal-soft/core/cpu_caps.cpp | 143 + .../love/src/jni/openal-soft/core/cpu_caps.h | 25 + .../src/jni/openal-soft/core/cubic_defs.h | 15 + .../src/jni/openal-soft/core/cubic_tables.cpp | 121 + .../src/jni/openal-soft/core/cubic_tables.h | 42 + .../src/jni/openal-soft/core/dbus_wrap.cpp | 42 + .../love/src/jni/openal-soft/core/dbus_wrap.h | 94 + .../src/jni/openal-soft/core/devformat.cpp | 69 + .../love/src/jni/openal-soft/core/devformat.h | 129 + .../love/src/jni/openal-soft/core/device.cpp | 20 + .../love/src/jni/openal-soft/core/device.h | 383 + .../src/jni/openal-soft/core/effects/base.h | 223 + .../src/jni/openal-soft/core/effectslot.cpp | 15 + .../src/jni/openal-soft/core/effectslot.h | 84 + .../{common/alexcpt.cpp => core/except.cpp} | 13 +- .../love/src/jni/openal-soft/core/except.h | 32 + .../{alc => core}/filters/biquad.cpp | 24 +- .../{alc => core}/filters/biquad.h | 33 +- .../openal-soft/{alc => core}/filters/nfc.cpp | 150 +- .../openal-soft/{alc => core}/filters/nfc.h | 41 +- .../{alc => core}/filters/splitter.cpp | 88 +- .../jni/openal-soft/core/filters/splitter.h | 42 + .../openal-soft/{alc => core}/fmt_traits.cpp | 8 +- .../src/jni/openal-soft/core/fmt_traits.h | 69 + .../src/jni/openal-soft/core/fpu_ctrl.cpp | 90 + .../love/src/jni/openal-soft/core/fpu_ctrl.h | 32 + .../jni/openal-soft/core/front_stablizer.h | 32 + .../love/src/jni/openal-soft/core/helpers.cpp | 507 + .../love/src/jni/openal-soft/core/helpers.h | 26 + .../love/src/jni/openal-soft/core/hrtf.cpp | 1470 + .../love/src/jni/openal-soft/core/hrtf.h | 98 + .../love/src/jni/openal-soft/core/logging.cpp | 176 + .../love/src/jni/openal-soft/core/logging.h | 36 + .../src/jni/openal-soft/core/mastering.cpp | 428 + .../jni/openal-soft/{alc => core}/mastering.h | 54 +- .../love/src/jni/openal-soft/core/mixer.cpp | 96 + .../love/src/jni/openal-soft/core/mixer.h | 107 + .../src/jni/openal-soft/core/mixer/defs.h | 115 + .../src/jni/openal-soft/core/mixer/hrtfbase.h | 140 + .../src/jni/openal-soft/core/mixer/hrtfdefs.h | 53 + .../jni/openal-soft/core/mixer/mixer_c.cpp | 280 + .../jni/openal-soft/core/mixer/mixer_neon.cpp | 494 + .../jni/openal-soft/core/mixer/mixer_sse.cpp | 393 + .../jni/openal-soft/core/mixer/mixer_sse2.cpp | 214 + .../jni/openal-soft/core/mixer/mixer_sse3.cpp | 0 .../openal-soft/core/mixer/mixer_sse41.cpp | 219 + .../jni/openal-soft/core/resampler_limits.h | 12 + .../love/src/jni/openal-soft/core/rtkit.cpp | 236 + .../love/src/jni/openal-soft/core/rtkit.h | 71 + .../jni/openal-soft/core/storage_formats.cpp | 85 + .../jni/openal-soft/core/storage_formats.h | 54 + .../src/jni/openal-soft/core/uhjfilter.cpp | 720 + .../love/src/jni/openal-soft/core/uhjfilter.h | 242 + .../jni/openal-soft/{alc => core}/uiddefs.cpp | 14 +- .../love/src/jni/openal-soft/core/voice.cpp | 1340 + .../love/src/jni/openal-soft/core/voice.h | 285 + .../src/jni/openal-soft/core/voice_change.h | 27 + .../{hrtf_default.h => default_hrtf.txt} | 240196 ++++++++++----- .../love/src/jni/openal-soft/docs/3D7.1.txt | 15 +- .../love/src/jni/openal-soft/docs/ambdec.txt | 11 +- .../src/jni/openal-soft/docs/ambisonics.txt | 23 +- .../src/jni/openal-soft/docs/env-vars.txt | 36 +- .../love/src/jni/openal-soft/docs/hrtf.txt | 4 +- .../src/jni/openal-soft/examples/alconvolve.c | 75 +- .../src/jni/openal-soft/examples/aldebug.cpp | 309 + .../src/jni/openal-soft/examples/aldirect.cpp | 472 + .../src/jni/openal-soft/examples/alffplay.cpp | 1303 +- .../src/jni/openal-soft/examples/alhrtf.c | 6 +- .../jni/openal-soft/examples/allafplay.cpp | 1021 + .../src/jni/openal-soft/examples/allatency.c | 6 +- .../src/jni/openal-soft/examples/alloopback.c | 12 +- .../jni/openal-soft/examples/almultireverb.c | 16 +- .../src/jni/openal-soft/examples/alplay.c | 187 +- .../src/jni/openal-soft/examples/alrecord.c | 2 +- .../src/jni/openal-soft/examples/alreverb.c | 22 +- .../src/jni/openal-soft/examples/alstream.c | 255 +- .../jni/openal-soft/examples/alstreamcb.cpp | 360 +- .../src/jni/openal-soft/examples/altonegen.c | 96 +- .../openal-soft/examples/common/alhelpers.c | 35 + .../openal-soft/examples/common/alhelpers.h | 64 +- .../src/jni/openal-soft/hrtf/Default HRTF.mhr | Bin 80353 -> 159841 bytes .../love/src/jni/openal-soft/include/AL/al.h | 508 +- .../love/src/jni/openal-soft/include/AL/alc.h | 149 +- .../src/jni/openal-soft/include/AL/alext.h | 722 +- .../jni/openal-soft/include/AL/efx-presets.h | 2 + .../love/src/jni/openal-soft/include/AL/efx.h | 134 +- .../src/jni/openal-soft/libopenal.version | 192 + .../src/jni/openal-soft/presets/3D7.1.ambdec | 71 +- .../jni/openal-soft/presets/hex-quad.ambdec | 53 + .../src/jni/openal-soft/presets/itu5.1.ambdec | 27 +- .../src/jni/openal-soft/presets/presets.txt | 18 +- .../love/src/jni/openal-soft/router/al.cpp | 72 +- .../love/src/jni/openal-soft/router/alc.cpp | 754 +- .../src/jni/openal-soft/router/router.cpp | 512 +- .../love/src/jni/openal-soft/router/router.h | 136 +- .../src/jni/openal-soft/tests/CMakeLists.txt | 24 + .../src/jni/openal-soft/tests/example.t.cpp | 16 + .../love/src/jni/openal-soft/utils/CIAIR.def | 7780 +- .../utils/alsoft-config/CMakeLists.txt | 10 +- .../utils/alsoft-config/mainwindow.cpp | 873 +- .../utils/alsoft-config/mainwindow.h | 40 +- .../utils/alsoft-config/mainwindow.ui | 367 +- .../love/src/jni/openal-soft/utils/getopt.c | 137 - .../love/src/jni/openal-soft/utils/getopt.h | 26 - .../jni/openal-soft/utils/makemhr/loaddef.cpp | 1066 +- .../jni/openal-soft/utils/makemhr/loaddef.h | 9 +- .../openal-soft/utils/makemhr/loadsofa.cpp | 319 +- .../jni/openal-soft/utils/makemhr/loadsofa.h | 6 +- .../jni/openal-soft/utils/makemhr/makemhr.cpp | 1204 +- .../jni/openal-soft/utils/makemhr/makemhr.h | 86 +- .../src/jni/openal-soft/utils/openal-info.c | 306 +- .../src/jni/openal-soft/utils/sofa-info.cpp | 81 +- .../jni/openal-soft/utils/sofa-support.cpp | 29 +- .../src/jni/openal-soft/utils/sofa-support.h | 4 +- .../src/jni/openal-soft/utils/uhjdecoder.cpp | 549 + .../src/jni/openal-soft/utils/uhjencoder.cpp | 593 + .../love/src/jni/openal-soft/version.h | 15 +- .../java/org/love2d/android/GameActivity.java | 179 + mobile/ios/patch_love_src.py | 278 + src/core/ChipAudio.lua | 54 +- src/core/Music.lua | 23 + src/core/Sound.lua | 8 +- tests/engine/audio_device_reset.lua | 283 + 1216 files changed, 264324 insertions(+), 147175 deletions(-) create mode 100644 mobile/android/love/src/jni/oboe/.github/ISSUE_TEMPLATE/bug_report.md create mode 100644 mobile/android/love/src/jni/oboe/.github/dependabot.yml create mode 100644 mobile/android/love/src/jni/oboe/.github/workflows/build-ci.yml create mode 100644 mobile/android/love/src/jni/oboe/.github/workflows/update-docs.yml delete mode 100644 mobile/android/love/src/jni/oboe/CONTRIBUTING rename mobile/android/love/src/jni/{openal-soft/alc/mixer/mixer_sse3.cpp => oboe/MODULE_LICENSE_APACHE2} (100%) delete mode 100644 mobile/android/love/src/jni/oboe/README create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/FormatConverterBox.cpp create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/FormatConverterBox.h delete mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/FullDuplexGlitches.h delete mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/FullDuplexLatency.cpp delete mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/FullDuplexLatency.h delete mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/FullDuplexStream.cpp delete mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/FullDuplexStream.h create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/FullDuplexStreamWithConversion.cpp create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/FullDuplexStreamWithConversion.h create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/OboeTesterStreamCallback.cpp create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/OboeTesterStreamCallback.h rename mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/{FullDuplexGlitches.cpp => OboeTools.h} (61%) create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/TestColdStartLatency.cpp create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/TestColdStartLatency.h create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/TestErrorCallback.cpp create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/TestErrorCallback.h create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/TestRapidCycle.cpp create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/TestRapidCycle.h create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/TestRoutingCrash.cpp create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/TestRoutingCrash.h create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/analyzer/BaseSineAnalyzer.h create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/analyzer/DataPathAnalyzer.h create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/flowunits/WhiteNoise.cpp create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/flowunits/WhiteNoise.h create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/BiquadFilter.h create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/DifferentiatedParabola.h create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/EnvelopeADSR.h create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/IncludeMeOnce.h create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/LookupTable.h create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/PitchToFrequency.h create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/SawtoothOscillator.h create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/SawtoothOscillatorDPW.h create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/SimpleVoice.h create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/SineOscillator.h create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/SquareOscillatorDPW.h create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/SynthTools.h create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/Synthesizer.h create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/UnitGenerator.h create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/VoiceBase.h create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/ic_launcher-playstore.png delete mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AudioMidiTester.java delete mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AutoGlitchActivity.java delete mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/BufferSizeView.java delete mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/ManualGlitchActivity.java delete mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/NativeEngine.java delete mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/RoundTripLatencyActivity.java delete mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/StreamConfiguration.java delete mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/StreamConfigurationView.java delete mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TapToToneActivity.java delete mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestAudioActivity.java delete mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestDisconnectActivity.java delete mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestOutputActivity.java rename mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/{google/sample => mobileer}/audio_device/AudioDeviceAdapter.java (91%) rename mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/{google/sample => mobileer}/audio_device/AudioDeviceInfoConverter.java (56%) rename mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/{google/sample => mobileer}/audio_device/AudioDeviceListEntry.java (85%) rename mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/{google/sample => mobileer}/audio_device/AudioDeviceSpinner.java (87%) create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/audio_device/CommunicationDeviceSpinner.java rename mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/{google/sample/oboe/manualtest => mobileer/oboetester}/AnalyzerActivity.java (51%) create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioForegroundService.java rename mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/{google/sample/oboe/manualtest => mobileer/oboetester}/AudioInputTester.java (96%) rename mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/{google/sample/oboe/manualtest => mobileer/oboetester}/AudioOutputTester.java (80%) create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioQueryTools.java rename mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/{google/sample/oboe/manualtest => mobileer/oboetester}/AudioRecordThread.java (91%) rename mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/{google/sample/oboe/manualtest => mobileer/oboetester}/AudioStreamBase.java (55%) rename mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/{google/sample/oboe/manualtest => mobileer/oboetester}/AudioStreamTester.java (93%) create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AutomatedGlitchActivity.java create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AutomatedTestRunner.java create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/BaseAutoGlitchActivity.java create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/BaseOboeTesterActivity.java create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/BufferSizeView.java create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/CachedTextViewLog.java rename mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/{google/sample/oboe/manualtest => mobileer/oboetester}/CircularCaptureBuffer.java (98%) create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/CommunicationDeviceView.java create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/DeviceReportActivity.java create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/DoubleStatistics.java create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/DynamicWorkloadActivity.java rename mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/{google/sample/oboe/manualtest => mobileer/oboetester}/EchoActivity.java (63%) rename mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/{google/sample/oboe/manualtest => mobileer/oboetester}/ExponentialTaper.java (97%) create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ExternalFileWriter.java create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ExternalTapToToneActivity.java create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ExtraTestsActivity.java rename mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/{google/sample/oboe/manualtest => mobileer/oboetester}/FastButton.java (91%) rename mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/{google/sample/oboe/manualtest => mobileer/oboetester}/GlitchActivity.java (51%) rename mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/{google/sample/oboe/manualtest => mobileer/oboetester}/InputMarginView.java (93%) create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/IntentBasedTestSupport.java rename mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/{google/sample/oboe/manualtest => mobileer/oboetester}/MainActivity.java (54%) create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ManualGlitchActivity.java create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/MicrophoneInfoConverter.java create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/MidiTapTester.java create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/MultiLineChart.java create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/NativeEngine.java create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/NativeSniffer.java rename mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/{google/sample/oboe/manualtest => mobileer/oboetester}/OboeAudioInputStream.java (94%) rename mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/{google/sample/oboe/manualtest => mobileer/oboetester}/OboeAudioOutputStream.java (70%) rename mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/{google/sample/oboe/manualtest => mobileer/oboetester}/OboeAudioStream.java (60%) rename mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/{google/sample/oboe/manualtest => mobileer/oboetester}/RecorderActivity.java (95%) create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/RoundTripLatencyActivity.java create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/StreamConfiguration.java create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/StreamConfigurationView.java rename mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/{google/sample/oboe/manualtest => mobileer/oboetester}/TapLatencyAnalyser.java (72%) create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TapToToneActivity.java create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TapToToneTester.java create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestAudioActivity.java create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestColdStartLatencyActivity.java create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestDataPathsActivity.java create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestDisconnectActivity.java create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestErrorCallbackActivity.java rename mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/{google/sample/oboe/manualtest => mobileer/oboetester}/TestInputActivity.java (65%) create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestOutputActivity.java rename mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/{google/sample/oboe/manualtest => mobileer/oboetester}/TestOutputActivityBase.java (50%) create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestPlugLatencyActivity.java create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestRapidCycleActivity.java create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestRouteDuringCallbackActivity.java rename mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/{google/sample/oboe/manualtest => mobileer/oboetester}/VolumeBarView.java (54%) rename mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/{google/sample/oboe/manualtest => mobileer/oboetester}/WaveformView.java (78%) rename mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/{google/sample/oboe/manualtest => mobileer/oboetester}/WorkloadView.java (77%) create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/res/drawable/ic_notification.xml create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/res/layout/activity_cold_start_latency.xml create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/res/layout/activity_data_paths.xml create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/res/layout/activity_device_report.xml create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/res/layout/activity_dynamic_workload.xml create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/res/layout/activity_error_callback.xml create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/res/layout/activity_external_tap_to_tone.xml create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/res/layout/activity_extra_tests.xml create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/res/layout/activity_rapid_cycle.xml create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/res/layout/activity_routing_crash.xml create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/res/layout/activity_test_plug_latency.xml create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/res/layout/auto_test_runner.xml create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/res/layout/comm_device_view.xml create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/res/layout/merge_audio_simple.xml create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/res/layout/sample_multi_line_chart.xml create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml delete mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/res/mipmap-hdpi/ic_launcher_round.png delete mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/res/mipmap-mdpi/ic_launcher_round.png delete mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png delete mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png delete mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/res/values-v21/styles.xml create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/res/values/attrs_multi_line_chart.xml create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/res/xml/button_color_selector.xml create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/docs/PrivacyPolicy.md create mode 100644 mobile/android/love/src/jni/oboe/apps/OboeTester/docs/TestOutput.md mode change 100644 => 100755 mobile/android/love/src/jni/oboe/apps/OboeTester/gradlew rename mobile/android/love/src/jni/oboe/apps/fxlab/app/src/main/{java => kotlin}/com/mobileer/androidfxlab/EffectsAdapter.kt (97%) rename mobile/android/love/src/jni/oboe/apps/fxlab/app/src/main/{java => kotlin}/com/mobileer/androidfxlab/MainActivity.kt (98%) rename mobile/android/love/src/jni/oboe/apps/fxlab/app/src/main/{java => kotlin}/com/mobileer/androidfxlab/NativeInterface.kt (100%) rename mobile/android/love/src/jni/oboe/apps/fxlab/app/src/main/{java => kotlin}/com/mobileer/androidfxlab/datatype/Effect.kt (99%) rename mobile/android/love/src/jni/oboe/apps/fxlab/app/src/main/{java => kotlin}/com/mobileer/androidfxlab/datatype/EffectDescription.kt (100%) rename mobile/android/love/src/jni/oboe/apps/fxlab/app/src/main/{java => kotlin}/com/mobileer/androidfxlab/datatype/ParamDescription.kt (96%) mode change 100644 => 100755 mobile/android/love/src/jni/oboe/apps/fxlab/gradlew mode change 100644 => 100755 mobile/android/love/src/jni/oboe/build_all_android.sh delete mode 100644 mobile/android/love/src/jni/oboe/docs/AppsUsingOboe.md delete mode 100644 mobile/android/love/src/jni/oboe/docs/ChangeLog.md create mode 100644 mobile/android/love/src/jni/oboe/docs/OpenSLESMigration.md create mode 100644 mobile/android/love/src/jni/oboe/docs/PrivacyPolicy.md delete mode 100644 mobile/android/love/src/jni/oboe/docs/index.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/notes/README.md delete mode 100644 mobile/android/love/src/jni/oboe/docs/notes/disconnect.md delete mode 100644 mobile/android/love/src/jni/oboe/docs/notes/effects.md delete mode 100644 mobile/android/love/src/jni/oboe/docs/notes/rlsbuffer.md delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/_audio_stream_8h_source.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/_audio_stream_base_8h_source.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/_audio_stream_builder_8h_source.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/_audio_stream_callback_8h_source.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/_definitions_8h_source.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/_latency_tuner_8h_source.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/_oboe_8h_source.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/_result_with_value_8h_source.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/_stabilized_callback_8h_source.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/_utilities_8h_source.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/_version_8h_source.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/annotated.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/bc_s.png delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/bdwn.png delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/classes.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/classoboe_1_1_audio_stream-members.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/classoboe_1_1_audio_stream.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/classoboe_1_1_audio_stream.png delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/classoboe_1_1_audio_stream_base-members.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/classoboe_1_1_audio_stream_base.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/classoboe_1_1_audio_stream_base.png delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/classoboe_1_1_audio_stream_builder-members.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/classoboe_1_1_audio_stream_builder.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/classoboe_1_1_audio_stream_builder.png delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/classoboe_1_1_audio_stream_callback-members.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/classoboe_1_1_audio_stream_callback.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/classoboe_1_1_audio_stream_callback.png delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/classoboe_1_1_default_stream_values-members.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/classoboe_1_1_default_stream_values.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/classoboe_1_1_latency_tuner-members.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/classoboe_1_1_latency_tuner.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/classoboe_1_1_result_with_value-members.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/classoboe_1_1_result_with_value.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/classoboe_1_1_stabilized_callback-members.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/classoboe_1_1_stabilized_callback.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/classoboe_1_1_stabilized_callback.png delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/closed.png delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/deprecated.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/dir_768f6301d9838e45d679001914ab2803.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/dir_d44c64559bbebec7f509842c48db8b23.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/doc.png delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/doxygen.css delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/doxygen.png delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/dynsections.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/files.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/folderclosed.png delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/folderopen.png delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/functions.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/functions_func.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/functions_vars.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/hierarchy.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/index.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/jquery.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/menu.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/menudata.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/namespacemembers.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/namespacemembers_enum.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/namespacemembers_eval.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/namespacemembers_func.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/namespacemembers_vars.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/namespaceoboe.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/namespaces.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/nav_f.png delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/nav_g.png delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/nav_h.png delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/open.png delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/pages.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/all_0.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/all_0.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/all_1.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/all_1.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/all_10.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/all_10.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/all_11.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/all_11.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/all_12.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/all_12.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/all_13.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/all_13.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/all_2.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/all_2.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/all_3.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/all_3.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/all_4.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/all_4.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/all_5.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/all_5.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/all_6.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/all_6.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/all_7.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/all_7.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/all_8.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/all_8.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/all_9.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/all_9.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/all_a.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/all_a.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/all_b.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/all_b.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/all_c.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/all_c.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/all_d.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/all_d.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/all_e.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/all_e.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/all_f.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/all_f.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/classes_0.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/classes_0.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/classes_1.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/classes_1.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/classes_2.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/classes_2.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/classes_3.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/classes_3.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/classes_4.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/classes_4.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/classes_5.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/classes_5.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/classes_6.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/classes_6.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/close.png delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/enums_0.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/enums_0.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/enums_1.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/enums_1.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/enums_2.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/enums_2.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/enums_3.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/enums_3.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/enums_4.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/enums_4.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/enums_5.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/enums_5.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/enums_6.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/enums_6.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/enums_7.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/enums_7.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/enumvalues_0.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/enumvalues_0.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/enumvalues_1.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/enumvalues_1.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/enumvalues_2.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/enumvalues_2.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/enumvalues_3.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/enumvalues_3.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/enumvalues_4.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/enumvalues_4.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/enumvalues_5.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/enumvalues_5.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/enumvalues_6.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/enumvalues_6.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/enumvalues_7.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/enumvalues_7.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/enumvalues_8.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/enumvalues_8.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/enumvalues_9.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/enumvalues_9.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/enumvalues_a.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/enumvalues_a.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/enumvalues_b.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/enumvalues_b.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/enumvalues_c.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/enumvalues_c.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/enumvalues_d.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/enumvalues_d.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/enumvalues_e.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/enumvalues_e.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/functions_0.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/functions_0.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/functions_1.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/functions_1.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/functions_2.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/functions_2.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/functions_3.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/functions_3.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/functions_4.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/functions_4.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/functions_5.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/functions_5.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/functions_6.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/functions_6.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/functions_7.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/functions_7.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/functions_8.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/functions_8.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/functions_9.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/functions_9.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/functions_a.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/functions_a.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/functions_b.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/functions_b.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/functions_c.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/functions_c.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/functions_d.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/functions_d.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/functions_e.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/functions_e.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/mag_sel.png delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/namespaces_0.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/namespaces_0.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/nomatches.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/pages_0.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/pages_0.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/pages_1.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/pages_1.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/search.css delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/search.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/search_l.png delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/search_m.png delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/search_r.png delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/searchdata.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/variables_0.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/variables_0.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/variables_1.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/variables_1.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/variables_2.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/variables_2.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/variables_3.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/variables_3.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/variables_4.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/variables_4.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/variables_5.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/variables_5.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/variables_6.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/variables_6.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/variables_7.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/search/variables_7.js delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/splitbar.png delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/structoboe_1_1_frame_timestamp-members.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/structoboe_1_1_frame_timestamp.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/structoboe_1_1_stream_deleter_functor-members.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/structoboe_1_1_stream_deleter_functor.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/structoboe_1_1_version-members.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/structoboe_1_1_version.html delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/sync_off.png delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/sync_on.png delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/tab_a.png delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/tab_b.png delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/tab_h.png delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/tab_s.png delete mode 100644 mobile/android/love/src/jni/oboe/docs/reference/tabs.css rename mobile/android/love/src/jni/oboe/{src/common => include/oboe}/AudioClock.h (95%) rename mobile/android/love/src/jni/oboe/{src/fifo => include/oboe}/FifoBuffer.h (62%) rename mobile/android/love/src/jni/oboe/{src/fifo => include/oboe}/FifoControllerBase.h (74%) create mode 100644 mobile/android/love/src/jni/oboe/include/oboe/FullDuplexStream.h create mode 100644 mobile/android/love/src/jni/oboe/include/oboe/OboeExtensions.h mode change 100644 => 100755 mobile/android/love/src/jni/oboe/prefab_build.sh delete mode 100644 mobile/android/love/src/jni/oboe/samples/LiveEffect/src/main/cpp/FullDuplexStream.cpp delete mode 100644 mobile/android/love/src/jni/oboe/samples/LiveEffect/src/main/cpp/FullDuplexStream.h create mode 100644 mobile/android/love/src/jni/oboe/samples/LiveEffect/src/main/java/com/google/oboe/samples/liveEffect/DuplexStreamForegroundService.java rename mobile/android/love/src/jni/oboe/samples/LiveEffect/src/main/java/com/google/{sample/oboe => oboe/samples}/liveEffect/LiveEffectEngine.java (95%) rename mobile/android/love/src/jni/oboe/samples/LiveEffect/src/main/java/com/google/{sample/oboe => oboe/samples}/liveEffect/MainActivity.java (81%) delete mode 100644 mobile/android/love/src/jni/oboe/samples/LiveEffect/src/main/res/drawable/balance_seekbar.xml rename mobile/android/love/src/jni/oboe/samples/MegaDrone/src/main/java/com/{example/oboe => google/oboe/samples}/megadrone/MainActivity.java (98%) rename mobile/android/love/src/jni/oboe/samples/RhythmGame/src/main/java/com/google/oboe/{sample => samples}/rhythmgame/GameSurfaceView.java (98%) rename mobile/android/love/src/jni/oboe/samples/RhythmGame/src/main/java/com/google/oboe/{sample => samples}/rhythmgame/MainActivity.java (89%) rename mobile/android/love/src/jni/oboe/samples/RhythmGame/src/main/java/com/google/oboe/{sample => samples}/rhythmgame/RendererWrapper.java (96%) mode change 100644 => 100755 mobile/android/love/src/jni/oboe/samples/RhythmGame/test/run_tests.sh create mode 100644 mobile/android/love/src/jni/oboe/samples/SoundBoard/README.md create mode 100644 mobile/android/love/src/jni/oboe/samples/SoundBoard/build.gradle create mode 100644 mobile/android/love/src/jni/oboe/samples/SoundBoard/proguard-rules.pro create mode 100644 mobile/android/love/src/jni/oboe/samples/SoundBoard/soundboard_image.png create mode 100644 mobile/android/love/src/jni/oboe/samples/SoundBoard/src/main/AndroidManifest.xml create mode 100644 mobile/android/love/src/jni/oboe/samples/SoundBoard/src/main/cpp/CMakeLists.txt create mode 100644 mobile/android/love/src/jni/oboe/samples/SoundBoard/src/main/cpp/SoundBoardEngine.cpp create mode 100644 mobile/android/love/src/jni/oboe/samples/SoundBoard/src/main/cpp/SoundBoardEngine.h create mode 100644 mobile/android/love/src/jni/oboe/samples/SoundBoard/src/main/cpp/Synth.h create mode 100644 mobile/android/love/src/jni/oboe/samples/SoundBoard/src/main/cpp/native-lib.cpp create mode 100644 mobile/android/love/src/jni/oboe/samples/SoundBoard/src/main/ic_launcher-playstore.png create mode 100644 mobile/android/love/src/jni/oboe/samples/SoundBoard/src/main/kotlin/com/google/oboe/samples/soundboard/MainActivity.kt create mode 100644 mobile/android/love/src/jni/oboe/samples/SoundBoard/src/main/kotlin/com/google/oboe/samples/soundboard/MusicTileView.kt create mode 100644 mobile/android/love/src/jni/oboe/samples/SoundBoard/src/main/kotlin/com/google/oboe/samples/soundboard/NoteListener.kt create mode 100644 mobile/android/love/src/jni/oboe/samples/SoundBoard/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 mobile/android/love/src/jni/oboe/samples/SoundBoard/src/main/res/drawable/ic_launcher_background.xml create mode 100644 mobile/android/love/src/jni/oboe/samples/SoundBoard/src/main/res/layout/activity_main.xml create mode 100644 mobile/android/love/src/jni/oboe/samples/SoundBoard/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 mobile/android/love/src/jni/oboe/samples/SoundBoard/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 mobile/android/love/src/jni/oboe/samples/SoundBoard/src/main/res/mipmap-hdpi/ic_launcher_foreground.png create mode 100644 mobile/android/love/src/jni/oboe/samples/SoundBoard/src/main/res/mipmap-mdpi/ic_launcher_foreground.png create mode 100644 mobile/android/love/src/jni/oboe/samples/SoundBoard/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png create mode 100644 mobile/android/love/src/jni/oboe/samples/SoundBoard/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png create mode 100644 mobile/android/love/src/jni/oboe/samples/SoundBoard/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png create mode 100644 mobile/android/love/src/jni/oboe/samples/SoundBoard/src/main/res/values/colors.xml create mode 100644 mobile/android/love/src/jni/oboe/samples/SoundBoard/src/main/res/values/strings.xml create mode 100644 mobile/android/love/src/jni/oboe/samples/SoundBoard/src/main/res/values/styles.xml rename mobile/android/love/src/jni/oboe/samples/audio-device/src/main/java/com/google/{sample => oboe/samples}/audio_device/AudioDeviceAdapter.java (97%) rename mobile/android/love/src/jni/oboe/samples/audio-device/src/main/java/com/google/{sample => oboe/samples}/audio_device/AudioDeviceInfoConverter.java (99%) rename mobile/android/love/src/jni/oboe/samples/audio-device/src/main/java/com/google/{sample => oboe/samples}/audio_device/AudioDeviceListEntry.java (98%) rename mobile/android/love/src/jni/oboe/samples/audio-device/src/main/java/com/google/{sample => oboe/samples}/audio_device/AudioDeviceSpinner.java (98%) create mode 100644 mobile/android/love/src/jni/oboe/samples/drumthumper/drumthumper-screenshot.png create mode 100644 mobile/android/love/src/jni/oboe/samples/drumthumper/playstore/DrumSetFeatureImage.png create mode 100644 mobile/android/love/src/jni/oboe/samples/drumthumper/playstore/ScreenShotPhone.png create mode 100644 mobile/android/love/src/jni/oboe/samples/drumthumper/playstore/ScreenShotWithMix.png create mode 100644 mobile/android/love/src/jni/oboe/samples/drumthumper/playstore/drumset.jpg rename mobile/android/love/src/jni/oboe/samples/drumthumper/src/main/{java/com/google/oboe/sample => kotlin/com/plausibleaudio}/drumthumper/DrumPlayer.kt (72%) rename mobile/android/love/src/jni/oboe/samples/drumthumper/src/main/{java/com/google/oboe/sample => kotlin/com/plausibleaudio}/drumthumper/DrumThumperActivity.kt (84%) rename mobile/android/love/src/jni/oboe/samples/drumthumper/src/main/{java/com/google/oboe/sample => kotlin/com/plausibleaudio}/drumthumper/TriggerPad.kt (57%) mode change 100644 => 100755 mobile/android/love/src/jni/oboe/samples/gradlew create mode 100644 mobile/android/love/src/jni/oboe/samples/hello-oboe/src/main/java/com/google/oboe/samples/hellooboe/BackgroundRunner.java rename mobile/android/love/src/jni/oboe/samples/hello-oboe/src/main/java/com/google/{sample/oboe => oboe/samples}/hellooboe/MainActivity.java (55%) create mode 100644 mobile/android/love/src/jni/oboe/samples/hello-oboe/src/main/java/com/google/oboe/samples/hellooboe/PlaybackEngine.java delete mode 100644 mobile/android/love/src/jni/oboe/samples/hello-oboe/src/main/java/com/google/sample/oboe/hellooboe/PlaybackEngine.java create mode 100644 mobile/android/love/src/jni/oboe/samples/minimaloboe/.gitignore create mode 100644 mobile/android/love/src/jni/oboe/samples/minimaloboe/README.md create mode 100644 mobile/android/love/src/jni/oboe/samples/minimaloboe/build.gradle create mode 100644 mobile/android/love/src/jni/oboe/samples/minimaloboe/minimaloboe-screenshot.png create mode 100644 mobile/android/love/src/jni/oboe/samples/minimaloboe/proguard-rules.pro create mode 100644 mobile/android/love/src/jni/oboe/samples/minimaloboe/src/main/AndroidManifest.xml create mode 100644 mobile/android/love/src/jni/oboe/samples/minimaloboe/src/main/cpp/CMakeLists.txt create mode 100644 mobile/android/love/src/jni/oboe/samples/minimaloboe/src/main/cpp/MinimalOboeJNI.cpp create mode 100644 mobile/android/love/src/jni/oboe/samples/minimaloboe/src/main/cpp/SimpleNoiseMaker.cpp create mode 100644 mobile/android/love/src/jni/oboe/samples/minimaloboe/src/main/cpp/SimpleNoiseMaker.h create mode 100644 mobile/android/love/src/jni/oboe/samples/minimaloboe/src/main/kotlin/com/example/minimaloboe/AudioPlayer.kt create mode 100644 mobile/android/love/src/jni/oboe/samples/minimaloboe/src/main/kotlin/com/example/minimaloboe/MainActivity.kt create mode 100644 mobile/android/love/src/jni/oboe/samples/minimaloboe/src/main/kotlin/com/example/minimaloboe/ui/theme/Color.kt create mode 100644 mobile/android/love/src/jni/oboe/samples/minimaloboe/src/main/kotlin/com/example/minimaloboe/ui/theme/Shape.kt create mode 100644 mobile/android/love/src/jni/oboe/samples/minimaloboe/src/main/kotlin/com/example/minimaloboe/ui/theme/Theme.kt create mode 100644 mobile/android/love/src/jni/oboe/samples/minimaloboe/src/main/kotlin/com/example/minimaloboe/ui/theme/Type.kt create mode 100644 mobile/android/love/src/jni/oboe/samples/minimaloboe/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 mobile/android/love/src/jni/oboe/samples/minimaloboe/src/main/res/drawable/ic_launcher_background.xml create mode 100644 mobile/android/love/src/jni/oboe/samples/minimaloboe/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 mobile/android/love/src/jni/oboe/samples/minimaloboe/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 mobile/android/love/src/jni/oboe/samples/minimaloboe/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 mobile/android/love/src/jni/oboe/samples/minimaloboe/src/main/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 mobile/android/love/src/jni/oboe/samples/minimaloboe/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 mobile/android/love/src/jni/oboe/samples/minimaloboe/src/main/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 mobile/android/love/src/jni/oboe/samples/minimaloboe/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 mobile/android/love/src/jni/oboe/samples/minimaloboe/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 mobile/android/love/src/jni/oboe/samples/minimaloboe/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 mobile/android/love/src/jni/oboe/samples/minimaloboe/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 mobile/android/love/src/jni/oboe/samples/minimaloboe/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 mobile/android/love/src/jni/oboe/samples/minimaloboe/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 mobile/android/love/src/jni/oboe/samples/minimaloboe/src/main/res/values/colors.xml create mode 100644 mobile/android/love/src/jni/oboe/samples/minimaloboe/src/main/res/values/strings.xml create mode 100644 mobile/android/love/src/jni/oboe/samples/minimaloboe/src/main/res/values/themes.xml rename mobile/android/love/src/jni/oboe/samples/shared/{DefaultAudioStreamCallback.h => DefaultDataCallback.h} (77%) create mode 100644 mobile/android/love/src/jni/oboe/samples/shared/DefaultErrorCallback.h create mode 100644 mobile/android/love/src/jni/oboe/samples/shared/SynthSound.h create mode 100644 mobile/android/love/src/jni/oboe/src/aaudio/AAudioExtensions.h create mode 100644 mobile/android/love/src/jni/oboe/src/common/AdpfWrapper.cpp create mode 100644 mobile/android/love/src/jni/oboe/src/common/AdpfWrapper.h create mode 100644 mobile/android/love/src/jni/oboe/src/common/OboeExtensions.cpp create mode 100644 mobile/android/love/src/jni/oboe/src/common/SourceI24Caller.cpp create mode 100644 mobile/android/love/src/jni/oboe/src/common/SourceI24Caller.h create mode 100644 mobile/android/love/src/jni/oboe/src/common/SourceI32Caller.cpp create mode 100644 mobile/android/love/src/jni/oboe/src/common/SourceI32Caller.h create mode 100644 mobile/android/love/src/jni/oboe/src/flowgraph/ChannelCountConverter.cpp create mode 100644 mobile/android/love/src/jni/oboe/src/flowgraph/ChannelCountConverter.h create mode 100644 mobile/android/love/src/jni/oboe/src/flowgraph/FlowgraphUtilities.h create mode 100644 mobile/android/love/src/jni/oboe/src/flowgraph/Limiter.cpp create mode 100644 mobile/android/love/src/jni/oboe/src/flowgraph/Limiter.h create mode 100644 mobile/android/love/src/jni/oboe/src/flowgraph/MonoBlend.cpp create mode 100644 mobile/android/love/src/jni/oboe/src/flowgraph/MonoBlend.h create mode 100644 mobile/android/love/src/jni/oboe/src/flowgraph/MultiToManyConverter.cpp create mode 100644 mobile/android/love/src/jni/oboe/src/flowgraph/MultiToManyConverter.h create mode 100644 mobile/android/love/src/jni/oboe/src/flowgraph/MultiToMonoConverter.cpp create mode 100644 mobile/android/love/src/jni/oboe/src/flowgraph/MultiToMonoConverter.h create mode 100644 mobile/android/love/src/jni/oboe/src/flowgraph/SinkI32.cpp create mode 100644 mobile/android/love/src/jni/oboe/src/flowgraph/SinkI32.h create mode 100644 mobile/android/love/src/jni/oboe/src/flowgraph/SinkI8_24.cpp create mode 100644 mobile/android/love/src/jni/oboe/src/flowgraph/SinkI8_24.h create mode 100644 mobile/android/love/src/jni/oboe/src/flowgraph/SourceI32.cpp create mode 100644 mobile/android/love/src/jni/oboe/src/flowgraph/SourceI32.h create mode 100644 mobile/android/love/src/jni/oboe/src/flowgraph/SourceI8_24.cpp create mode 100644 mobile/android/love/src/jni/oboe/src/flowgraph/SourceI8_24.h create mode 100644 mobile/android/love/src/jni/oboe/src/flowgraph/resampler/ResamplerDefinitions.h delete mode 100644 mobile/android/love/src/jni/oboe/tests/README create mode 100644 mobile/android/love/src/jni/oboe/tests/UnitTestRunner/app/src/main/.gitignore mode change 100644 => 100755 mobile/android/love/src/jni/oboe/tests/UnitTestRunner/gradlew mode change 100644 => 100755 mobile/android/love/src/jni/oboe/tests/run_tests.sh create mode 100644 mobile/android/love/src/jni/oboe/tests/testAAudio.cpp create mode 100644 mobile/android/love/src/jni/oboe/tests/testAudioClock.cpp create mode 100644 mobile/android/love/src/jni/oboe/tests/testFullDuplexStream.cpp create mode 100644 mobile/android/love/src/jni/oboe/tests/testResampler.cpp create mode 100644 mobile/android/love/src/jni/oboe/tests/testReturnStop.cpp create mode 100644 mobile/android/love/src/jni/oboe/tests/testReturnStopDeadlock.cpp create mode 100644 mobile/android/love/src/jni/oboe/tests/testStreamFramesProcessed.cpp create mode 100644 mobile/android/love/src/jni/oboe/tests/testStreamStop.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/.github/workflows/ci.yml create mode 100644 mobile/android/love/src/jni/openal-soft/.github/workflows/utils.yml create mode 100644 mobile/android/love/src/jni/openal-soft/LICENSE-pffft create mode 100644 mobile/android/love/src/jni/openal-soft/OpenALConfig.cmake.in create mode 100644 mobile/android/love/src/jni/openal-soft/al/debug.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/al/debug.h create mode 100644 mobile/android/love/src/jni/openal-soft/al/direct_defs.h create mode 100644 mobile/android/love/src/jni/openal-soft/al/eax/api.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/al/eax/api.h create mode 100644 mobile/android/love/src/jni/openal-soft/al/eax/call.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/al/eax/call.h create mode 100644 mobile/android/love/src/jni/openal-soft/al/eax/effect.h create mode 100644 mobile/android/love/src/jni/openal-soft/al/eax/exception.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/al/eax/exception.h create mode 100644 mobile/android/love/src/jni/openal-soft/al/eax/fx_slot_index.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/al/eax/fx_slot_index.h create mode 100644 mobile/android/love/src/jni/openal-soft/al/eax/fx_slots.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/al/eax/fx_slots.h create mode 100644 mobile/android/love/src/jni/openal-soft/al/eax/globals.h create mode 100644 mobile/android/love/src/jni/openal-soft/al/eax/utils.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/al/eax/utils.h create mode 100644 mobile/android/love/src/jni/openal-soft/al/eax/x_ram.h create mode 100644 mobile/android/love/src/jni/openal-soft/al/effects/autowah.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/al/effects/chorus.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/al/effects/compressor.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/al/effects/convolution.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/al/effects/dedicated.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/al/effects/distortion.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/al/effects/echo.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/al/effects/effects.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/al/effects/effects.h create mode 100644 mobile/android/love/src/jni/openal-soft/al/effects/equalizer.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/al/effects/fshifter.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/al/effects/modulator.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/al/effects/null.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/al/effects/pshifter.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/al/effects/reverb.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/al/effects/vmorpher.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/al/error.h delete mode 100644 mobile/android/love/src/jni/openal-soft/alc/alcmain.h delete mode 100644 mobile/android/love/src/jni/openal-soft/alc/alcontext.h delete mode 100644 mobile/android/love/src/jni/openal-soft/alc/ambdec.cpp delete mode 100644 mobile/android/love/src/jni/openal-soft/alc/ambdec.h delete mode 100644 mobile/android/love/src/jni/openal-soft/alc/ambidefs.h create mode 100644 mobile/android/love/src/jni/openal-soft/alc/backends/otherio.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/alc/backends/otherio.h create mode 100644 mobile/android/love/src/jni/openal-soft/alc/backends/pipewire.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/alc/backends/pipewire.h delete mode 100644 mobile/android/love/src/jni/openal-soft/alc/bformatdec.cpp delete mode 100644 mobile/android/love/src/jni/openal-soft/alc/bformatdec.h delete mode 100644 mobile/android/love/src/jni/openal-soft/alc/bs2b.cpp delete mode 100644 mobile/android/love/src/jni/openal-soft/alc/bs2b.h delete mode 100644 mobile/android/love/src/jni/openal-soft/alc/bsinc_defs.h delete mode 100644 mobile/android/love/src/jni/openal-soft/alc/bsinc_tables.cpp delete mode 100644 mobile/android/love/src/jni/openal-soft/alc/bsinc_tables.h delete mode 100644 mobile/android/love/src/jni/openal-soft/alc/buffer_storage.cpp delete mode 100644 mobile/android/love/src/jni/openal-soft/alc/buffer_storage.h delete mode 100644 mobile/android/love/src/jni/openal-soft/alc/bufferline.h delete mode 100644 mobile/android/love/src/jni/openal-soft/alc/compat.h create mode 100644 mobile/android/love/src/jni/openal-soft/alc/context.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/alc/context.h delete mode 100644 mobile/android/love/src/jni/openal-soft/alc/converter.cpp delete mode 100644 mobile/android/love/src/jni/openal-soft/alc/converter.h delete mode 100644 mobile/android/love/src/jni/openal-soft/alc/cpu_caps.cpp delete mode 100644 mobile/android/love/src/jni/openal-soft/alc/cpu_caps.h delete mode 100644 mobile/android/love/src/jni/openal-soft/alc/devformat.h create mode 100644 mobile/android/love/src/jni/openal-soft/alc/device.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/alc/device.h create mode 100644 mobile/android/love/src/jni/openal-soft/alc/events.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/alc/events.h create mode 100644 mobile/android/love/src/jni/openal-soft/alc/export_list.h delete mode 100644 mobile/android/love/src/jni/openal-soft/alc/filters/splitter.h delete mode 100644 mobile/android/love/src/jni/openal-soft/alc/fmt_traits.h delete mode 100644 mobile/android/love/src/jni/openal-soft/alc/fpu_ctrl.cpp delete mode 100644 mobile/android/love/src/jni/openal-soft/alc/fpu_ctrl.h delete mode 100644 mobile/android/love/src/jni/openal-soft/alc/front_stablizer.h delete mode 100644 mobile/android/love/src/jni/openal-soft/alc/helpers.cpp delete mode 100644 mobile/android/love/src/jni/openal-soft/alc/hrtf.cpp delete mode 100644 mobile/android/love/src/jni/openal-soft/alc/hrtf.h delete mode 100644 mobile/android/love/src/jni/openal-soft/alc/logging.h delete mode 100644 mobile/android/love/src/jni/openal-soft/alc/mastering.cpp delete mode 100644 mobile/android/love/src/jni/openal-soft/alc/mixer/defs.h delete mode 100644 mobile/android/love/src/jni/openal-soft/alc/mixer/hrtfbase.h delete mode 100644 mobile/android/love/src/jni/openal-soft/alc/mixer/mixer_c.cpp delete mode 100644 mobile/android/love/src/jni/openal-soft/alc/mixer/mixer_neon.cpp delete mode 100644 mobile/android/love/src/jni/openal-soft/alc/mixer/mixer_sse.cpp delete mode 100644 mobile/android/love/src/jni/openal-soft/alc/mixer/mixer_sse2.cpp delete mode 100644 mobile/android/love/src/jni/openal-soft/alc/mixer/mixer_sse41.cpp delete mode 100644 mobile/android/love/src/jni/openal-soft/alc/ringbuffer.cpp delete mode 100644 mobile/android/love/src/jni/openal-soft/alc/ringbuffer.h delete mode 100644 mobile/android/love/src/jni/openal-soft/alc/uhjfilter.cpp delete mode 100644 mobile/android/love/src/jni/openal-soft/alc/uhjfilter.h delete mode 100644 mobile/android/love/src/jni/openal-soft/alc/voice.cpp delete mode 100644 mobile/android/love/src/jni/openal-soft/alc/voice.h delete mode 100644 mobile/android/love/src/jni/openal-soft/cmake/FindDSound.cmake delete mode 100644 mobile/android/love/src/jni/openal-soft/cmake/FindSDL2.cmake create mode 100644 mobile/android/love/src/jni/openal-soft/cmake/FindSndIO.cmake delete mode 100644 mobile/android/love/src/jni/openal-soft/cmake/FindSoundIO.cmake delete mode 100644 mobile/android/love/src/jni/openal-soft/cmake/FindWindowsSDK.cmake create mode 100644 mobile/android/love/src/jni/openal-soft/common/alassert.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/common/alassert.h create mode 100644 mobile/android/love/src/jni/openal-soft/common/albit.h delete mode 100644 mobile/android/love/src/jni/openal-soft/common/albyte.h delete mode 100644 mobile/android/love/src/jni/openal-soft/common/alexcpt.h delete mode 100644 mobile/android/love/src/jni/openal-soft/common/alfstream.cpp delete mode 100644 mobile/android/love/src/jni/openal-soft/common/alfstream.h delete mode 100644 mobile/android/love/src/jni/openal-soft/common/almalloc.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/common/alnumbers.h delete mode 100644 mobile/android/love/src/jni/openal-soft/common/aloptional.h rename mobile/android/love/src/jni/openal-soft/common/{threads.cpp => alsem.cpp} (66%) create mode 100644 mobile/android/love/src/jni/openal-soft/common/alsem.h create mode 100644 mobile/android/love/src/jni/openal-soft/common/althrd_setname.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/common/althrd_setname.h create mode 100644 mobile/android/love/src/jni/openal-soft/common/althreads.h create mode 100644 mobile/android/love/src/jni/openal-soft/common/altraits.h create mode 100644 mobile/android/love/src/jni/openal-soft/common/comptr.h delete mode 100644 mobile/android/love/src/jni/openal-soft/common/endiantest.h create mode 100644 mobile/android/love/src/jni/openal-soft/common/flexarray.h delete mode 100644 mobile/android/love/src/jni/openal-soft/common/math_defs.h create mode 100644 mobile/android/love/src/jni/openal-soft/common/pffft.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/common/pffft.h create mode 100644 mobile/android/love/src/jni/openal-soft/common/phase_shifter.h create mode 100644 mobile/android/love/src/jni/openal-soft/common/ringbuffer.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/common/ringbuffer.h delete mode 100644 mobile/android/love/src/jni/openal-soft/common/threads.h create mode 100644 mobile/android/love/src/jni/openal-soft/config_backends.h create mode 100644 mobile/android/love/src/jni/openal-soft/config_backends.h.in create mode 100644 mobile/android/love/src/jni/openal-soft/config_simd.h create mode 100644 mobile/android/love/src/jni/openal-soft/config_simd.h.in create mode 100644 mobile/android/love/src/jni/openal-soft/configs/HRTF/alsoft.ini create mode 100644 mobile/android/love/src/jni/openal-soft/core/ambdec.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/core/ambdec.h create mode 100644 mobile/android/love/src/jni/openal-soft/core/ambidefs.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/core/ambidefs.h create mode 100644 mobile/android/love/src/jni/openal-soft/core/async_event.h create mode 100644 mobile/android/love/src/jni/openal-soft/core/bformatdec.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/core/bformatdec.h create mode 100644 mobile/android/love/src/jni/openal-soft/core/bs2b.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/core/bs2b.h create mode 100644 mobile/android/love/src/jni/openal-soft/core/bsinc_defs.h create mode 100644 mobile/android/love/src/jni/openal-soft/core/bsinc_tables.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/core/bsinc_tables.h create mode 100644 mobile/android/love/src/jni/openal-soft/core/buffer_storage.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/core/buffer_storage.h create mode 100644 mobile/android/love/src/jni/openal-soft/core/bufferline.h create mode 100644 mobile/android/love/src/jni/openal-soft/core/context.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/core/context.h create mode 100644 mobile/android/love/src/jni/openal-soft/core/converter.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/core/converter.h create mode 100644 mobile/android/love/src/jni/openal-soft/core/cpu_caps.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/core/cpu_caps.h create mode 100644 mobile/android/love/src/jni/openal-soft/core/cubic_defs.h create mode 100644 mobile/android/love/src/jni/openal-soft/core/cubic_tables.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/core/cubic_tables.h create mode 100644 mobile/android/love/src/jni/openal-soft/core/dbus_wrap.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/core/dbus_wrap.h create mode 100644 mobile/android/love/src/jni/openal-soft/core/devformat.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/core/devformat.h create mode 100644 mobile/android/love/src/jni/openal-soft/core/device.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/core/device.h create mode 100644 mobile/android/love/src/jni/openal-soft/core/effects/base.h create mode 100644 mobile/android/love/src/jni/openal-soft/core/effectslot.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/core/effectslot.h rename mobile/android/love/src/jni/openal-soft/{common/alexcpt.cpp => core/except.cpp} (52%) create mode 100644 mobile/android/love/src/jni/openal-soft/core/except.h rename mobile/android/love/src/jni/openal-soft/{alc => core}/filters/biquad.cpp (87%) rename mobile/android/love/src/jni/openal-soft/{alc => core}/filters/biquad.h (83%) rename mobile/android/love/src/jni/openal-soft/{alc => core}/filters/nfc.cpp (79%) rename mobile/android/love/src/jni/openal-soft/{alc => core}/filters/nfc.h (55%) rename mobile/android/love/src/jni/openal-soft/{alc => core}/filters/splitter.cpp (51%) create mode 100644 mobile/android/love/src/jni/openal-soft/core/filters/splitter.h rename mobile/android/love/src/jni/openal-soft/{alc => core}/fmt_traits.cpp (97%) create mode 100644 mobile/android/love/src/jni/openal-soft/core/fmt_traits.h create mode 100644 mobile/android/love/src/jni/openal-soft/core/fpu_ctrl.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/core/fpu_ctrl.h create mode 100644 mobile/android/love/src/jni/openal-soft/core/front_stablizer.h create mode 100644 mobile/android/love/src/jni/openal-soft/core/helpers.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/core/helpers.h create mode 100644 mobile/android/love/src/jni/openal-soft/core/hrtf.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/core/hrtf.h create mode 100644 mobile/android/love/src/jni/openal-soft/core/logging.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/core/logging.h create mode 100644 mobile/android/love/src/jni/openal-soft/core/mastering.cpp rename mobile/android/love/src/jni/openal-soft/{alc => core}/mastering.h (70%) create mode 100644 mobile/android/love/src/jni/openal-soft/core/mixer.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/core/mixer.h create mode 100644 mobile/android/love/src/jni/openal-soft/core/mixer/defs.h create mode 100644 mobile/android/love/src/jni/openal-soft/core/mixer/hrtfbase.h create mode 100644 mobile/android/love/src/jni/openal-soft/core/mixer/hrtfdefs.h create mode 100644 mobile/android/love/src/jni/openal-soft/core/mixer/mixer_c.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/core/mixer/mixer_neon.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/core/mixer/mixer_sse.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/core/mixer/mixer_sse2.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/core/mixer/mixer_sse3.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/core/mixer/mixer_sse41.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/core/resampler_limits.h create mode 100644 mobile/android/love/src/jni/openal-soft/core/rtkit.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/core/rtkit.h create mode 100644 mobile/android/love/src/jni/openal-soft/core/storage_formats.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/core/storage_formats.h create mode 100644 mobile/android/love/src/jni/openal-soft/core/uhjfilter.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/core/uhjfilter.h rename mobile/android/love/src/jni/openal-soft/{alc => core}/uiddefs.cpp (64%) create mode 100644 mobile/android/love/src/jni/openal-soft/core/voice.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/core/voice.h create mode 100644 mobile/android/love/src/jni/openal-soft/core/voice_change.h rename mobile/android/love/src/jni/openal-soft/{hrtf_default.h => default_hrtf.txt} (50%) create mode 100644 mobile/android/love/src/jni/openal-soft/examples/aldebug.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/examples/aldirect.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/examples/allafplay.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/libopenal.version create mode 100644 mobile/android/love/src/jni/openal-soft/presets/hex-quad.ambdec create mode 100644 mobile/android/love/src/jni/openal-soft/tests/CMakeLists.txt create mode 100644 mobile/android/love/src/jni/openal-soft/tests/example.t.cpp delete mode 100644 mobile/android/love/src/jni/openal-soft/utils/getopt.c delete mode 100644 mobile/android/love/src/jni/openal-soft/utils/getopt.h create mode 100644 mobile/android/love/src/jni/openal-soft/utils/uhjdecoder.cpp create mode 100644 mobile/android/love/src/jni/openal-soft/utils/uhjencoder.cpp create mode 100644 tests/engine/audio_device_reset.lua diff --git a/main.lua b/main.lua index eb0768c6..1a9cd3aa 100644 --- a/main.lua +++ b/main.lua @@ -808,6 +808,25 @@ 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.touchpressed(id, x, y, dx, dy, pressure) if editorMode then -- iOS synthesizes mousepressed for the primary touch; forwarding here diff --git a/mobile/android/love/src/jni/love/src/common/android.cpp b/mobile/android/love/src/jni/love/src/common/android.cpp index 3c327be4..82834d7f 100644 --- a/mobile/android/love/src/jni/love/src/common/android.cpp +++ b/mobile/android/love/src/jni/love/src/common/android.cpp @@ -45,6 +45,10 @@ // 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" + namespace love { namespace android @@ -1320,4 +1324,70 @@ 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::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(); +} + #endif // LOVE_ANDROID diff --git a/mobile/android/love/src/jni/love/src/modules/audio/openal/Audio.cpp b/mobile/android/love/src/jni/love/src/modules/audio/openal/Audio.cpp index c1d0c198..829dc91e 100644 --- a/mobile/android/love/src/jni/love/src/modules/audio/openal/Audio.cpp +++ b/mobile/android/love/src/jni/love/src/modules/audio/openal/Audio.cpp @@ -22,6 +22,7 @@ #include "common/delay.h" #include "RecordingDevice.h" #include "sound/Decoder.h" +#include "event/Event.h" #include #include @@ -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(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 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) diff --git a/mobile/android/love/src/jni/love/src/modules/audio/openal/Audio.h b/mobile/android/love/src/jni/love/src/modules/audio/openal/Audio.h index 85df0c4a..90ae94d6 100644 --- a/mobile/android/love/src/jni/love/src/modules/audio/openal/Audio.h +++ b/mobile/android/love/src/jni/love/src/modules/audio/openal/Audio.h @@ -22,6 +22,7 @@ #define LOVE_AUDIO_OPENAL_AUDIO_H // STD +#include #include #include #include @@ -97,6 +98,8 @@ public: std::vector 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 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; diff --git a/mobile/android/love/src/jni/oboe/.github/ISSUE_TEMPLATE/bug_report.md b/mobile/android/love/src/jni/oboe/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..b1558121 --- /dev/null +++ b/mobile/android/love/src/jni/oboe/.github/ISSUE_TEMPLATE/bug_report.md @@ -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. diff --git a/mobile/android/love/src/jni/oboe/.github/dependabot.yml b/mobile/android/love/src/jni/oboe/.github/dependabot.yml new file mode 100644 index 00000000..709bcf56 --- /dev/null +++ b/mobile/android/love/src/jni/oboe/.github/dependabot.yml @@ -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" diff --git a/mobile/android/love/src/jni/oboe/.github/workflows/build-ci.yml b/mobile/android/love/src/jni/oboe/.github/workflows/build-ci.yml new file mode 100644 index 00000000..b8a0858e --- /dev/null +++ b/mobile/android/love/src/jni/oboe/.github/workflows/build-ci.yml @@ -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 diff --git a/mobile/android/love/src/jni/oboe/.github/workflows/update-docs.yml b/mobile/android/love/src/jni/oboe/.github/workflows/update-docs.yml new file mode 100644 index 00000000..611200db --- /dev/null +++ b/mobile/android/love/src/jni/oboe/.github/workflows/update-docs.yml @@ -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 diff --git a/mobile/android/love/src/jni/oboe/.gitignore b/mobile/android/love/src/jni/oboe/.gitignore index 508af1b1..76e7cb3c 100644 --- a/mobile/android/love/src/jni/oboe/.gitignore +++ b/mobile/android/love/src/jni/oboe/.gitignore @@ -4,3 +4,5 @@ .cxx/ .idea build +.logpile + diff --git a/mobile/android/love/src/jni/oboe/Android.mk b/mobile/android/love/src/jni/oboe/Android.mk index c4a641a3..01958be9 100644 --- a/mobile/android/love/src/jni/oboe/Android.mk +++ b/mobile/android/love/src/jni/oboe/Android.mk @@ -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) diff --git a/mobile/android/love/src/jni/oboe/CMakeLists.txt b/mobile/android/love/src/jni/oboe/CMakeLists.txt index 8a6b632e..55e3984d 100644 --- a/mobile/android/love/src/jni/oboe/CMakeLists.txt +++ b/mobile/android/love/src/jni/oboe/CMakeLists.txt @@ -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 + "$<$:-Ofast>" + "$<$:-O3>" "$<$:-Werror>") # Enable logging of D,V for debug builds target_compile_definitions(oboe PUBLIC $<$:OBOE_ENABLE_LOGGING=1>) +option(OBOE_DO_NOT_DEFINE_OPENSL_ES_CONSTANTS "Do not define OpenSLES constants" OFF) +target_compile_definitions(oboe PRIVATE $<$: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/ 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) \ No newline at end of file +install(DIRECTORY include/oboe DESTINATION include) diff --git a/mobile/android/love/src/jni/oboe/CONTRIBUTING b/mobile/android/love/src/jni/oboe/CONTRIBUTING deleted file mode 100644 index 5d9ff17d..00000000 --- a/mobile/android/love/src/jni/oboe/CONTRIBUTING +++ /dev/null @@ -1 +0,0 @@ -Please see the CONTRIBUTING.md file for more information. diff --git a/mobile/android/love/src/jni/oboe/Doxyfile b/mobile/android/love/src/jni/oboe/Doxyfile index 12b0b596..6b87a122 100644 --- a/mobile/android/love/src/jni/oboe/Doxyfile +++ b/mobile/android/love/src/jni/oboe/Doxyfile @@ -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 diff --git a/mobile/android/love/src/jni/openal-soft/alc/mixer/mixer_sse3.cpp b/mobile/android/love/src/jni/oboe/MODULE_LICENSE_APACHE2 similarity index 100% rename from mobile/android/love/src/jni/openal-soft/alc/mixer/mixer_sse3.cpp rename to mobile/android/love/src/jni/oboe/MODULE_LICENSE_APACHE2 diff --git a/mobile/android/love/src/jni/oboe/README b/mobile/android/love/src/jni/oboe/README deleted file mode 100644 index 9150edf7..00000000 --- a/mobile/android/love/src/jni/oboe/README +++ /dev/null @@ -1 +0,0 @@ -Please see the README.md file for more information. diff --git a/mobile/android/love/src/jni/oboe/README.md b/mobile/android/love/src/jni/oboe/README.md index 3507bffc..37713380 100644 --- a/mobile/android/love/src/jni/oboe/README.md +++ b/mobile/android/love/src/jni/oboe/README.md @@ -1,4 +1,4 @@ -# Oboe [![Build Status](https://travis-ci.org/google/oboe.svg?branch=master)](https://travis-ci.org/google/oboe) +# Oboe [![Build CI](https://github.com/google/oboe/workflows/Build%20CI/badge.svg)](https://github.com/google/oboe/actions) [![Introduction to Oboe video](docs/images/getting-started-video.jpg)](https://www.youtube.com/watch?v=csfHAbr5ilI&list=PLWz5rJ2EKKc_duWv9IPNvx9YBudNMmLSa) @@ -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) diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/.gitignore b/mobile/android/love/src/jni/oboe/apps/OboeTester/.gitignore index e698dc3c..7112e857 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/.gitignore +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/.gitignore @@ -6,6 +6,8 @@ /build/ .idea/ /app/build/ +/app/release/ +/app/debug/ /app/app.iml *.iml /app/externalNativeBuild/ diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/CMakeLists.txt b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/CMakeLists.txt index a414d9c9..a47e6abf 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/CMakeLists.txt +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/CMakeLists.txt @@ -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") diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/build.gradle b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/build.gradle index b131b79e..860c51be 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/build.gradle +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/build.gradle @@ -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' -} \ No newline at end of file + androidTestImplementation 'androidx.test.ext:junit:1.1.5' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' +} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/AndroidManifest.xml b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/AndroidManifest.xml index e0cc3d19..1bd168ba 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/AndroidManifest.xml +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/AndroidManifest.xml @@ -1,100 +1,138 @@ - - - - - - + + + + + + - - - - + + + - + android:theme="@style/AppTheme" + android:requestLegacyExternalStorage="true" + android:banner="@mipmap/ic_launcher"> + android:screenOrientation="portrait" + android:exported="true"> + - - - + android:screenOrientation="portrait" /> - - + android:screenOrientation="portrait" /> - - + android:screenOrientation="portrait" /> - - + android:screenOrientation="portrait" /> - - + android:screenOrientation="portrait" /> - - + android:screenOrientation="portrait" /> - - + android:screenOrientation="portrait" /> - - + android:name=".AutomatedGlitchActivity" + android:label="@string/title_activity_auto_glitches" + android:screenOrientation="portrait" /> - + android:screenOrientation="portrait" /> + + + + + + + + + + + + android:name=".MidiTapTester" + android:permission="android.permission.BIND_MIDI_DEVICE_SERVICE" + android:exported="true"> @@ -104,16 +142,21 @@ android:resource="@xml/service_device_info" /> + + + + android:resource="@xml/provider_paths" /> - - \ No newline at end of file + diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/AudioStreamGateway.cpp b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/AudioStreamGateway.cpp index e89f9142..6867d140 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/AudioStreamGateway.cpp +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/AudioStreamGateway.cpp @@ -17,29 +17,24 @@ #include #include +#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; -} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/AudioStreamGateway.h b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/AudioStreamGateway.h index 2451b01a..0aaf429a 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/AudioStreamGateway.h +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/AudioStreamGateway.h @@ -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 sink) { + void setAudioSink(std::shared_ptr 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 mAudioSink; + + std::shared_ptr mAudioSink; }; diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/FormatConverterBox.cpp b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/FormatConverterBox.cpp new file mode 100644 index 00000000..0974b4c4 --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/FormatConverterBox.cpp @@ -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(maxSamples * sizeof(int32_t)); + mOutputBuffer = std::make_unique(maxSamples * sizeof(int32_t)); + + mSource.reset(); + switch (mInputFormat) { + case oboe::AudioFormat::I16: + case oboe::AudioFormat::IEC61937: + mSource = std::make_unique(1); + break; + case oboe::AudioFormat::I24: + mSource = std::make_unique(1); + break; + case oboe::AudioFormat::I32: + mSource = std::make_unique(1); + break; + case oboe::AudioFormat::Float: + case oboe::AudioFormat::Invalid: + case oboe::AudioFormat::Unspecified: + mSource = std::make_unique(1); + break; + } + + mSink.reset(); + switch (mOutputFormat) { + case oboe::AudioFormat::I16: + case oboe::AudioFormat::IEC61937: + mSink = std::make_unique(1); + break; + case oboe::AudioFormat::I24: + mSink = std::make_unique(1); + break; + case oboe::AudioFormat::I32: + mSink = std::make_unique(1); + break; + case oboe::AudioFormat::Float: + case oboe::AudioFormat::Invalid: + case oboe::AudioFormat::Unspecified: + mSink = std::make_unique(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); +} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/FormatConverterBox.h b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/FormatConverterBox.h new file mode 100644 index 00000000..4a673b3a --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/FormatConverterBox.h @@ -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 +#include + +#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 mInputBuffer; + std::unique_ptr mOutputBuffer; + + std::unique_ptr mSource; + std::unique_ptr mSink; +}; + + +#endif //OBOETESTER_FORMAT_CONVERTER_BOX_H diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.cpp b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.cpp index c8a99acf..fe872d72 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.cpp +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.cpp @@ -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(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; diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.h b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.h index ee12fc17..82b1abed 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.h +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.h @@ -21,40 +21,52 @@ #include #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 mWriteReadDeltaValid{false}; + std::atomic mWriteReadDelta{0}; }; diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/FullDuplexEcho.cpp b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/FullDuplexEcho.cpp index 0da2d002..d54f669d 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/FullDuplexEcho.cpp +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/FullDuplexEcho.cpp @@ -20,28 +20,46 @@ oboe::Result FullDuplexEcho::start() { int32_t delayFrames = (int32_t) (kMaxDelayTimeSeconds * getOutputStream()->getSampleRate()); mDelayLine = std::make_unique(delayFrames); - return FullDuplexStream::start(); + // Use peak detector for input streams + mNumChannels = getInputStream()->getChannelCount(); + mPeakDetectors = std::make_unique(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(inputData); + float *outputFloat = outputData; // zero out entire output array - memset(outputFloat, 0, numOutputFrames * getOutputStream()->getBytesPerFrame()); + memset(outputFloat, 0, static_cast(numOutputFrames) + * static_cast(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; } diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/FullDuplexEcho.h b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/FullDuplexEcho.h index 47f7d5ff..a88e51ef 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/FullDuplexEcho.h +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/FullDuplexEcho.h @@ -21,28 +21,31 @@ #include #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 mDelayLine; static constexpr double kMaxDelayTimeSeconds = 4.0; double mDelayTimeSeconds = kMaxDelayTimeSeconds; + std::atomic mNumChannels{0}; + std::unique_ptr mPeakDetectors; + }; diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/FullDuplexGlitches.h b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/FullDuplexGlitches.h deleted file mode 100644 index 1046e04b..00000000 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/FullDuplexGlitches.h +++ /dev/null @@ -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 -#include - -#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 - diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/FullDuplexLatency.cpp b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/FullDuplexLatency.cpp deleted file mode 100644 index 34036782..00000000 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/FullDuplexLatency.cpp +++ /dev/null @@ -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 - -#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; -}; diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/FullDuplexLatency.h b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/FullDuplexLatency.h deleted file mode 100644 index a47217a9..00000000 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/FullDuplexLatency.h +++ /dev/null @@ -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 -#include - -#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 diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/FullDuplexStream.cpp b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/FullDuplexStream.cpp deleted file mode 100644 index af233aaf..00000000 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/FullDuplexStream.cpp +++ /dev/null @@ -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 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 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 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 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 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(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; -} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/FullDuplexStream.h b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/FullDuplexStream.h deleted file mode 100644 index ccb52177..00000000 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/FullDuplexStream.h +++ /dev/null @@ -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 -#include - -#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 mInputBuffer; -}; - - -#endif //OBOETESTER_FULL_DUPLEX_STREAM_H diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/FullDuplexStreamWithConversion.cpp b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/FullDuplexStreamWithConversion.cpp new file mode 100644 index 00000000..a6184c3c --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/FullDuplexStreamWithConversion.cpp @@ -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(inputBufferSize, + getInputStream()->getFormat(), + oboe::AudioFormat::Float); + mOutputConverter = std::make_unique(outputBufferSize, + oboe::AudioFormat::Float, + getOutputStream()->getFormat()); + return FullDuplexStream::start(); +} + +oboe::ResultWithValue FullDuplexStreamWithConversion::readInput(int32_t numFrames) { + oboe::ResultWithValue 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(mInputConverter->getOutputBuffer()), + numInputFrames, + static_cast(mOutputConverter->getInputBuffer()), + numOutputFrames); + mOutputConverter->convertFromInternalInput( outputData, + numOutputFrames * getOutputStream()->getChannelCount()); + return callbackResult; +} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/FullDuplexStreamWithConversion.h b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/FullDuplexStreamWithConversion.h new file mode 100644 index 00000000..63c2242e --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/FullDuplexStreamWithConversion.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. + */ + +#ifndef OBOETESTER_FULL_DUPLEX_STREAM_WITH_CONVERSION_H +#define OBOETESTER_FULL_DUPLEX_STREAM_WITH_CONVERSION_H + +#include +#include + +#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 readInput(int32_t numFrames) override; + + virtual oboe::Result start() override; + +private: + std::unique_ptr mInputConverter; + std::unique_ptr mOutputConverter; + +}; + + +#endif //OBOETESTER_FULL_DUPLEX_STREAM_WITH_CONVERSION_H diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/InputStreamCallbackAnalyzer.cpp b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/InputStreamCallbackAnalyzer.cpp index d04f09af..73c1bbed 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/InputStreamCallbackAnalyzer.cpp +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/InputStreamCallbackAnalyzer.cpp @@ -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); } } diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/InputStreamCallbackAnalyzer.h b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/InputStreamCallbackAnalyzer.h index 42ffbfbf..07c4f979 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/InputStreamCallbackAnalyzer.h +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/InputStreamCallbackAnalyzer.h @@ -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(channelCount); + int32_t bufferSize = maxFramesPerCallback * channelCount; + mInputConverter = std::make_unique(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 mPeakDetectors; + MultiChannelRecording *mRecording = nullptr; private: - int32_t mMinimumFramesBeforeRead = 0; + std::unique_ptr mInputConverter; + int32_t mMinimumFramesBeforeRead = 0; }; #endif //NATIVEOBOE_INPUTSTREAMCALLBACKANALYZER_H diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/InterpolatingDelayLine.cpp b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/InterpolatingDelayLine.cpp index e69c7112..f5393c57 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/InterpolatingDelayLine.cpp +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/InterpolatingDelayLine.cpp @@ -14,7 +14,8 @@ * limitations under the License. */ -#include "common/OboeDebug.h" +#include + #include "InterpolatingDelayLine.h" InterpolatingDelayLine::InterpolatingDelayLine(int32_t delaySize) { diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/InterpolatingDelayLine.h b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/InterpolatingDelayLine.h index b3a510da..b05e23fb 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/InterpolatingDelayLine.h +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/InterpolatingDelayLine.h @@ -21,9 +21,6 @@ #include #include -#include "oboe/Oboe.h" -#include "FullDuplexStream.h" - /** * Monophonic delay line. */ diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp index 00266107..ec1d9017 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp @@ -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 #include +#if DEBUG_CLOSE_RACE +#include +#endif // DEBUG_CLOSE_RACE #include +#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 ActivityContext::getOutputStream() { for (auto entry : mOboeStreams) { - oboe::AudioStream *oboeStream = entry.second.get(); + std::shared_ptr 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 ActivityContext::getInputStream() { for (auto entry : mOboeStreams) { - oboe::AudioStream *oboeStream = entry.second.get(); + std::shared_ptr 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 oboeStream = entry.second; + result = oboeStream->release(); + } + return result; +} + void ActivityContext::close(int32_t streamIndex) { stopBlockingIOThread(); - oboe::AudioStream *oboeStream = getStream(streamIndex); + std::shared_ptr 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 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 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 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 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(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 inputStream = getInputStream(); + std::shared_ptr 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 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 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(mChannelCount); - mSinkFloat = std::make_unique(mChannelCount); - mSinkI16 = std::make_unique(mChannelCount); + std::shared_ptr outputStream = getOutputStream(); - oboe::AudioStream *outputStream = getOutputStream(); + mVolumeRamp = std::make_shared(mChannelCount); + mVolumeRamp->setLengthInFrames(kRampMSec * outputStream->getSampleRate() / + MILLISECONDS_PER_SECOND); + mVolumeRamp->setTarget(mAmplitude); + + mSinkFloat = std::make_shared(mChannelCount); + mSinkI16 = std::make_shared(mChannelCount); + mSinkI24 = std::make_shared(mChannelCount); + mSinkI32 = std::make_shared(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 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 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 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(mChannelCount); - mSinkFloat = std::make_unique(mChannelCount); - mSinkI16 = std::make_unique(mChannelCount); + mSinkFloat = std::make_shared(mChannelCount); + mSinkI16 = std::make_shared(mChannelCount); + mSinkI24 = std::make_shared(mChannelCount); + mSinkI32 = std::make_shared(mChannelCount); - oboe::AudioStream *outputStream = getOutputStream(); + std::shared_ptr 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 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 &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(); + mFullDuplexLatency = std::make_unique(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 + &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(); + mFullDuplexGlitches = std::make_unique(&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 &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(&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 &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 outputStream = getOutputStream(); + std::shared_ptr inputStream = getInputStream(); if (outputStream) { mSinkFloat = std::make_unique(mChannelCount); sineOscillator = std::make_unique(); @@ -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); } diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h index ed989cd9..ffd5b861 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h @@ -17,7 +17,6 @@ #ifndef NATIVEOBOE_NATIVEAUDIOCONTEXT_H #define NATIVEOBOE_NATIVEAUDIOCONTEXT_H -#include #include #include #include @@ -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 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 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 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 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 getInputStream(); + std::shared_ptr 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 &oboeStream) {} virtual oboe::Result startStreams() = 0; @@ -332,9 +333,11 @@ protected: int32_t mSampleRate = 0; // TODO per stream std::atomic 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 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 mVolumeRamp; std::unique_ptr manyToMulti; std::unique_ptr monoToMulti; - std::shared_ptr mSinkFloat; - std::shared_ptr mSinkI16; + std::shared_ptr mSinkFloat; + std::shared_ptr mSinkI16; + std::shared_ptr mSinkI24; + std::shared_ptr 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 &oboeStream) override; private: std::unique_ptr 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(); +#else + // Old analyzer based on encoded random bits. + mLatencyAnalyzer = std::make_unique(); +#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 &oboeStream) override; private: - std::unique_ptr mFullDuplexLatency{}; + std::unique_ptr mFullDuplexLatency{}; + + std::unique_ptr 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 &oboeStream) override; private: - std::unique_ptr mFullDuplexGlitches{}; + std::unique_ptr 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 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(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 &oboeStream) override; + +private: + std::unique_ptr 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 outputStream = getOutputStream(); if (outputStream) { return outputStream->start(); } - oboe::AudioStream *inputStream = getInputStream(); + std::shared_ptr inputStream = getInputStream(); if (inputStream) { return inputStream->start(); } return oboe::Result::ErrorNull; } - void configureForStart() override; + void configureAfterOpen() override; private: std::unique_ptr sineOscillator; std::unique_ptr monoToMulti; - std::shared_ptr mSinkFloat; + std::shared_ptr 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 diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/OboeStreamCallbackProxy.cpp b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/OboeStreamCallbackProxy.cpp index f0855da4..01814ec3 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/OboeStreamCallbackProxy.cpp +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/OboeStreamCallbackProxy.cpp @@ -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(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; } diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/OboeStreamCallbackProxy.h b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/OboeStreamCallbackProxy.h index 3964e1fb..9b5f7530 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/OboeStreamCallbackProxy.h +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/OboeStreamCallbackProxy.h @@ -19,15 +19,129 @@ #include #include +#include #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 skipCount { 0 }; + std::atomic sum { 0 }; + std::atomic count { 0 }; + std::atomic minimum { DBL_MAX }; + std::atomic 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 mCpuLoad{0}; + static constexpr double kNsToMsScaler = 0.000001; + std::atomic mCpuLoad{0.0f}; + std::atomic 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 mFramesPerCallback{0}; + + std::atomic mCpuAffinityMask{0}; + std::atomic mPreviousMask{0}; + std::atomic mCpuMask{0}; + cpu_set_t mOriginalCpuSet; + bool mIsOriginalCpuSetValid = false; + }; - #endif //NATIVEOBOE_OBOESTREAMCALLBACKPROXY_H diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/OboeTesterStreamCallback.cpp b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/OboeTesterStreamCallback.cpp new file mode 100644 index 00000000..5109f67c --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/OboeTesterStreamCallback.cpp @@ -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 +#include + +#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; +} \ No newline at end of file diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/OboeTesterStreamCallback.h b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/OboeTesterStreamCallback.h new file mode 100644 index 00000000..70a719ea --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/OboeTesterStreamCallback.h @@ -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 +#include +#include +#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 diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/FullDuplexGlitches.cpp b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/OboeTools.h similarity index 61% rename from mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/FullDuplexGlitches.cpp rename to mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/OboeTools.h index 53cfa5b2..c8b47f84 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/FullDuplexGlitches.cpp +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/OboeTools.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 diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/SawPingGenerator.cpp b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/SawPingGenerator.cpp index 341999c6..3220df22 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/SawPingGenerator.cpp +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/SawPingGenerator.cpp @@ -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++; } diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/SawPingGenerator.h b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/SawPingGenerator.h index 981bad63..f1f5e5b3 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/SawPingGenerator.h +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/SawPingGenerator.h @@ -32,7 +32,9 @@ public: int32_t onProcess(int numFrames) override; - void setEnabled(bool enabled); + void trigger(); + + void reset() override; private: std::atomic mRequestCount; // external thread increments this to request a beep diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/TestColdStartLatency.cpp b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/TestColdStartLatency.cpp new file mode 100644 index 00000000..9b4f25ca --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/TestColdStartLatency.cpp @@ -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 +#include + +#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(); + + // 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; +} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/TestColdStartLatency.h b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/TestColdStartLatency.h new file mode 100644 index 00000000..7c70687e --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/TestColdStartLatency.h @@ -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 + +/** + * 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 mBeginStartNanos{0}; + std::atomic mOpenTimeMicros{0}; + std::atomic mStartTimeMicros{0}; + std::atomic mColdStartTimeMicros{0}; + std::atomic 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 mStream; + std::shared_ptr mDataCallback; + + static constexpr int kChannelCount = 1; +}; + +#endif //OBOETESTER_TEST_COLD_START_LATENCY_H diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/TestErrorCallback.cpp b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/TestErrorCallback.cpp new file mode 100644 index 00000000..ce4285f5 --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/TestErrorCallback.cpp @@ -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 + +#include "common/OboeDebug.h" +#include "TestErrorCallback.h" + +using namespace oboe; + +oboe::Result TestErrorCallback::open() { + mCallbackMagic = 0; + mDataCallback = std::make_shared(); + mErrorCallback = std::make_shared(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; +} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/TestErrorCallback.h b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/TestErrorCallback.h new file mode 100644 index 00000000..a7611fe4 --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/TestErrorCallback.h @@ -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 + +/** + * 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 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 mMagic{kMagicGood}; + }; + + std::shared_ptr mStream; + std::shared_ptr mDataCallback; + std::shared_ptr mErrorCallback; + + static constexpr int kChannelCount = 2; +}; + +#endif //OBOETESTER_TEST_ERROR_CALLBACK_H diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/TestRapidCycle.cpp b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/TestRapidCycle.cpp new file mode 100644 index 00000000..4e6a9cb0 --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/TestRapidCycle.cpp @@ -0,0 +1,97 @@ +/* + * 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 +#include + +#include "common/OboeDebug.h" +#include "oboe/AudioClock.h" +#include "TestRapidCycle.h" + +using namespace oboe; + +// start a thread to cycle through stream tests +int32_t TestRapidCycle::start(bool useOpenSL) { + mThreadEnabled = true; + mCycleCount = 0; + mCycleThread = std::thread([this, useOpenSL]() { + cycleRapidly(useOpenSL); + }); + return 0; +} +int32_t TestRapidCycle::stop() { + mThreadEnabled = false; + mCycleThread.join(); + return 0; +} + +void TestRapidCycle::cycleRapidly(bool useOpenSL) { + while(mThreadEnabled && (oneCycle(useOpenSL) == 0)); +} + +int32_t TestRapidCycle::oneCycle(bool useOpenSL) { + mCycleCount++; + mDataCallback = std::make_shared(); + + AudioStreamBuilder builder; + oboe::Result result = builder.setFormat(oboe::AudioFormat::Float) + ->setAudioApi(useOpenSL ? oboe::AudioApi::OpenSLES : oboe::AudioApi::AAudio) + ->setPerformanceMode(oboe::PerformanceMode::LowLatency) + ->setChannelCount(kChannelCount) + ->setDataCallback(mDataCallback) + ->setUsage(oboe::Usage::Notification) + ->openStream(mStream); + if (result != oboe::Result::OK) { + return (int32_t) result; + } + + mStream->setDelayBeforeCloseMillis(0); + + result = mStream->requestStart(); + if (result != oboe::Result::OK) { + mStream->close(); + return (int32_t) result; + } +// Sleep for some random time. + int32_t durationMicros = (int32_t)(drand48() * kMaxSleepMicros); + LOGD("TestRapidCycle::oneCycle() - Sleep for %d micros", durationMicros); + usleep(durationMicros); + LOGD("TestRapidCycle::oneCycle() - Woke up, close stream"); + mDataCallback->returnStop = true; + result = mStream->close(); + return (int32_t) result; +} + +// Callback that sleeps then touches the audio buffer. +DataCallbackResult TestRapidCycle::MyDataCallback::onAudioReady( + AudioStream *audioStream, + void *audioData, + int32_t numFrames) { + float *floatData = (float *) audioData; + const int numSamples = numFrames * kChannelCount; + + // Fill buffer with white noise. + for (int i = 0; i < numSamples; i++) { + floatData[i] = ((float) drand48() - 0.5f) * 2 * 0.1f; + } + usleep(500); // half a millisecond + if (returnStop) { + usleep(20 * 1000); + return DataCallbackResult::Stop; + } else { + return DataCallbackResult::Continue; + } +} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/TestRapidCycle.h b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/TestRapidCycle.h new file mode 100644 index 00000000..772933df --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/TestRapidCycle.h @@ -0,0 +1,67 @@ +/* + * 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_RAPID_CYCLE_H +#define OBOETESTER_TEST_RAPID_CYCLE_H + +#include "oboe/Oboe.h" +#include + + +/** + * Try to cause a crash by changing routing during a data callback. + * We use Use::VoiceCommunication for the stream and + * setSpeakerPhoneOn(b) to force a routing change. + * This works best when connected to a BT headset. + */ +class TestRapidCycle { +public: + + int32_t start(bool useOpenSL); + int32_t stop(); + + int32_t getCycleCount() { + return mCycleCount.load(); + } + +private: + + void cycleRapidly(bool useOpenSL); + int32_t oneCycle(bool useOpenSL); + + class MyDataCallback : public oboe::AudioStreamDataCallback { public: + + MyDataCallback() {} + + oboe::DataCallbackResult onAudioReady( + oboe::AudioStream *audioStream, + void *audioData, + int32_t numFrames) override; + + bool returnStop = false; + }; + + std::shared_ptr mStream; + std::shared_ptr mDataCallback; + std::atomic mCycleCount{0}; + std::atomic mThreadEnabled{false}; + std::thread mCycleThread; + + static constexpr int kChannelCount = 1; + static constexpr int kMaxSleepMicros = 25000; +}; + +#endif //OBOETESTER_TEST_RAPID_CYCLE_H diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/TestRoutingCrash.cpp b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/TestRoutingCrash.cpp new file mode 100644 index 00000000..0a0c30ba --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/TestRoutingCrash.cpp @@ -0,0 +1,111 @@ +/* + * 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 +#include + +#include "common/OboeDebug.h" +#include "oboe/AudioClock.h" +#include "TestRoutingCrash.h" + +using namespace oboe; + +// open start start an Oboe stream +int32_t TestRoutingCrash::start(bool useInput) { + + mDataCallback = std::make_shared(this); + + // Disable MMAP because we are trying to crash a Legacy Stream. + bool wasMMapEnabled = AAudioExtensions::getInstance().isMMapEnabled(); + AAudioExtensions::getInstance().setMMapEnabled(false); + + AudioStreamBuilder builder; + oboe::Result result = builder.setFormat(oboe::AudioFormat::Float) +#if 1 + ->setPerformanceMode(oboe::PerformanceMode::LowLatency) +#else + ->setPerformanceMode(oboe::PerformanceMode::None) +#endif + ->setDirection(useInput ? oboe::Direction::Input : oboe::Direction::Output) + ->setChannelCount(kChannelCount) + ->setDataCallback(mDataCallback) + // Use VoiceCommunication so we can reroute it by setting SpeakerPhone ON/OFF. + ->setUsage(oboe::Usage::VoiceCommunication) + ->openStream(mStream); + if (result != oboe::Result::OK) { + return (int32_t) result; + } + + AAudioExtensions::getInstance().setMMapEnabled(wasMMapEnabled); + return (int32_t) mStream->requestStart(); +} + +int32_t TestRoutingCrash::stop() { + oboe::Result result1 = mStream->requestStop(); + oboe::Result result2 = mStream->close(); + return (int32_t)((result1 != oboe::Result::OK) ? result1 : result2); +} + +// Callback that sleeps then touches the audio buffer. +DataCallbackResult TestRoutingCrash::MyDataCallback::onAudioReady( + AudioStream *audioStream, + void *audioData, + int32_t numFrames) { + float *floatData = (float *) audioData; + + // If I call getTimestamp() here it does NOT crash! + + // Simulate the timing of a heavy workload by sleeping. + // Otherwise the window for the crash is very narrow. + const double kDutyCycle = 0.7; + const double bufferTimeNanos = 1.0e9 * numFrames / (double) audioStream->getSampleRate(); + const int64_t targetDurationNanos = (int64_t) (bufferTimeNanos * kDutyCycle); + if (targetDurationNanos > 0) { + AudioClock::sleepForNanos(targetDurationNanos); + } + const double kFilterCoefficient = 0.95; // low pass IIR filter + const double sleepMicros = targetDurationNanos * 0.0001; + mParent->averageSleepTimeMicros = ((1.0 - kFilterCoefficient) * sleepMicros) + + (kFilterCoefficient * mParent->averageSleepTimeMicros); + + // If I call getTimestamp() here it crashes. + audioStream->getTimestamp(CLOCK_MONOTONIC); // Trigger a restoreTrack_l() in framework. + + const int numSamples = numFrames * kChannelCount; + if (audioStream->getDirection() == oboe::Direction::Input) { + // Read buffer and write sum of samples to a member variable. + // We just want to touch the memory and not get optimized away by the compiler. + float sum = 0.0f; + for (int i = 0; i < numSamples; i++) { + sum += *floatData++; + } + mInputSum = sum; + } else { + // Fill mono buffer with a sine wave. + // If the routing occurred then the buffer may be dead and + // we may be writing into unallocated memory. + for (int i = 0; i < numSamples; i++) { + *floatData++ = sinf(mPhase) * 0.2f; + mPhase += kPhaseIncrement; + // Wrap the phase around in a circle. + if (mPhase >= M_PI) mPhase -= 2 * M_PI; + } + } + + // If I call getTimestamp() here it does NOT crash! + + return oboe::DataCallbackResult::Continue; +} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/TestRoutingCrash.h b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/TestRoutingCrash.h new file mode 100644 index 00000000..bddc24ac --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/TestRoutingCrash.h @@ -0,0 +1,67 @@ +/* + * 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_ROUTING_CRASH_H +#define OBOETESTER_TEST_ROUTING_CRASH_H + +#include "oboe/Oboe.h" +#include + +/** + * Try to cause a crash by changing routing during a data callback. + * We use Use::VoiceCommunication for the stream and + * setSpeakerPhoneOn(b) to force a routing change. + * This works best when connected to a BT headset. + */ +class TestRoutingCrash { +public: + + int32_t start(bool useInput); + int32_t stop(); + + int32_t getSleepTimeMicros() { + return (int32_t) (averageSleepTimeMicros.load()); + } + +protected: + + std::atomic averageSleepTimeMicros{0}; + +private: + + class MyDataCallback : public oboe::AudioStreamDataCallback { public: + + MyDataCallback(TestRoutingCrash *parent): mParent(parent) {} + + oboe::DataCallbackResult onAudioReady( + oboe::AudioStream *audioStream, + void *audioData, + int32_t numFrames) override; + private: + TestRoutingCrash *mParent; + // For sine generator. + float mPhase = 0.0f; + static constexpr float kPhaseIncrement = 2.0f * (float) M_PI * 440.0f / 48000.0f; + float mInputSum = 0.0f; // For saving input data sum to prevent over-optimization. + }; + + std::shared_ptr mStream; + std::shared_ptr mDataCallback; + + static constexpr int kChannelCount = 1; +}; + +#endif //OBOETESTER_TEST_ROUTING_CRASH_H diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/analyzer/BaseSineAnalyzer.h b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/analyzer/BaseSineAnalyzer.h new file mode 100644 index 00000000..1ecece52 --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/analyzer/BaseSineAnalyzer.h @@ -0,0 +1,240 @@ +/* + * Copyright (C) 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 ANALYZER_BASE_SINE_ANALYZER_H +#define ANALYZER_BASE_SINE_ANALYZER_H + +#include +#include +#include +#include + +#include "InfiniteRecording.h" +#include "LatencyAnalyzer.h" + +/** + * Output a steady sine wave and analyze the return signal. + * + * Use a cosine transform to measure the predicted magnitude and relative phase of the + * looped back sine wave. Then generate a predicted signal and compare with the actual signal. + */ +class BaseSineAnalyzer : public LoopbackProcessor { +public: + + BaseSineAnalyzer() + : LoopbackProcessor() + , mInfiniteRecording(64 * 1024) {} + + virtual bool isOutputEnabled() { return true; } + + void setMagnitude(double magnitude) { + mMagnitude = magnitude; + mScaledTolerance = mMagnitude * getTolerance(); + } + + /** + * + * @return valid phase or kPhaseInvalid=-999 + */ + double getPhaseOffset() { + ALOGD("%s(), mPhaseOffset = %f\n", __func__, mPhaseOffset); + return mPhaseOffset; + } + + double getMagnitude() const { + return mMagnitude; + } + + void setNoiseAmplitude(double noiseAmplitude) { + mNoiseAmplitude = noiseAmplitude; + } + + double getNoiseAmplitude() const { + return mNoiseAmplitude; + } + + double getTolerance() { + return mTolerance; + } + + void setTolerance(double tolerance) { + mTolerance = tolerance; + } + + // advance and wrap phase + void incrementInputPhase() { + mInputPhase += mPhaseIncrement; + if (mInputPhase > M_PI) { + mInputPhase -= (2.0 * M_PI); + } + } + + // advance and wrap phase + void incrementOutputPhase() { + mOutputPhase += mPhaseIncrement; + if (mOutputPhase > M_PI) { + mOutputPhase -= (2.0 * M_PI); + } + } + + + /** + * @param frameData upon return, contains the reference sine wave + * @param channelCount + */ + result_code processOutputFrame(float *frameData, int channelCount) override { + float output = 0.0f; + // Output sine wave so we can measure it. + if (isOutputEnabled()) { + float sinOut = sinf(mOutputPhase); + incrementOutputPhase(); + output = (sinOut * mOutputAmplitude) + + (mWhiteNoise.nextRandomDouble() * getNoiseAmplitude()); + // ALOGD("sin(%f) = %f, %f\n", mOutputPhase, sinOut, kPhaseIncrement); + } + for (int i = 0; i < channelCount; i++) { + frameData[i] = (i == getOutputChannel()) ? output : 0.0f; + } + return RESULT_OK; + } + + /** + * Calculate the magnitude of the component of the input signal + * that matches the analysis frequency. + * Also calculate the phase that we can use to create a + * signal that matches that component. + * The phase will be between -PI and +PI. + */ + double calculateMagnitudePhase(double *phasePtr = nullptr) { + if (mFramesAccumulated == 0) { + return 0.0; + } + double sinMean = mSinAccumulator / mFramesAccumulated; + double cosMean = mCosAccumulator / mFramesAccumulated; + double magnitude = 2.0 * sqrt((sinMean * sinMean) + (cosMean * cosMean)); + if (phasePtr != nullptr) { + double phase; + if (magnitude < kMinValidMagnitude) { + phase = kPhaseInvalid; + ALOGD("%s() mag very low! sinMean = %7.5f, cosMean = %7.5f", + __func__, sinMean, cosMean); + } else { + phase = atan2(cosMean, sinMean); + if (phase == 0.0) { + ALOGD("%s() phase zero! sinMean = %7.5f, cosMean = %7.5f", + __func__, sinMean, cosMean); + } + } + *phasePtr = phase; + } + return magnitude; + } + + /** + * Perform sin/cos analysis on each sample. + * Measure magnitude and phase on every period. + * Updates mPhaseOffset + * @param sample + * @param referencePhase + * @return true if magnitude and phase updated + */ + bool transformSample(float sample) { + // Compare incoming signal with the reference input sine wave. + mSinAccumulator += static_cast(sample) * sinf(mInputPhase); + mCosAccumulator += static_cast(sample) * cosf(mInputPhase); + incrementInputPhase(); + + mFramesAccumulated++; + // Must be a multiple of the period or the calculation will not be accurate. + if (mFramesAccumulated == mSinePeriod) { + const double coefficient = 0.1; + double magnitude = calculateMagnitudePhase(&mPhaseOffset); + + ALOGD("%s(), phaseOffset = %f\n", __func__, mPhaseOffset); + if (mPhaseOffset != kPhaseInvalid) { + // One pole averaging filter. + setMagnitude((mMagnitude * (1.0 - coefficient)) + (magnitude * coefficient)); + } + resetAccumulator(); + return true; + } else { + return false; + } + } + + // reset the sine wave detector + virtual void resetAccumulator() { + mFramesAccumulated = 0; + mSinAccumulator = 0.0; + mCosAccumulator = 0.0; + } + + void reset() override { + LoopbackProcessor::reset(); + resetAccumulator(); + mMagnitude = 0.0; + } + + void prepareToTest() override { + LoopbackProcessor::prepareToTest(); + mSinePeriod = getSampleRate() / kTargetGlitchFrequency; + mInputPhase = 0.0f; + mOutputPhase = 0.0f; + mInverseSinePeriod = 1.0 / mSinePeriod; + mPhaseIncrement = 2.0 * M_PI * mInverseSinePeriod; + } + +protected: + // Try to get a prime period so the waveform plot changes every time. + static constexpr int32_t kTargetGlitchFrequency = 48000 / 113; + + int32_t mSinePeriod = 1; // this will be set before use + double mInverseSinePeriod = 1.0; + double mPhaseIncrement = 0.0; + // Use two sine wave phases, input and output. + // This is because the number of input and output samples may differ + // in a callback and the output frame count may advance ahead of the input, or visa versa. + double mInputPhase = 0.0; + double mOutputPhase = 0.0; + double mOutputAmplitude = 0.75; + // This is the phase offset between the mInputPhase sine wave and the recorded + // signal at the tuned frequency. + // If this jumps around then we are probably just hearing noise. + // Noise can cause the magnitude to be high but mPhaseOffset will be pretty random. + // If we are tracking a sine wave then mPhaseOffset should be consistent. + double mPhaseOffset = 0.0; + // kPhaseInvalid indicates that the phase measurement cannot be used. + // We were seeing times when a magnitude of zero was causing atan2(s,c) to + // return a phase of zero, which looked valid to Java. This is a way of passing + // an error code back to Java as a single value to avoid race conditions. + static constexpr double kPhaseInvalid = -999.0; + double mMagnitude = 0.0; + static constexpr double kMinValidMagnitude = 2.0 / (1 << 16); + int32_t mFramesAccumulated = 0; + double mSinAccumulator = 0.0; + double mCosAccumulator = 0.0; + double mScaledTolerance = 0.0; + + InfiniteRecording mInfiniteRecording; + +private: + float mTolerance = 0.10; // scaled from 0.0 to 1.0 + + float mNoiseAmplitude = 0.00; // Used to experiment with warbling caused by DRC. + PseudoRandom mWhiteNoise; +}; + +#endif //ANALYZER_BASE_SINE_ANALYZER_H diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/analyzer/DataPathAnalyzer.h b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/analyzer/DataPathAnalyzer.h new file mode 100644 index 00000000..deabaa96 --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/analyzer/DataPathAnalyzer.h @@ -0,0 +1,106 @@ +/* + * Copyright (C) 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 ANALYZER_DATA_PATH_ANALYZER_H +#define ANALYZER_DATA_PATH_ANALYZER_H + +#include +#include +#include +#include +#include + +#include "BaseSineAnalyzer.h" +#include "InfiniteRecording.h" +#include "LatencyAnalyzer.h" + +/** + * Output a steady sine wave and analyze the return signal. + * + * Use a cosine transform to measure the predicted magnitude and relative phase of the + * looped back sine wave. + */ +class DataPathAnalyzer : public BaseSineAnalyzer { +public: + + DataPathAnalyzer() : BaseSineAnalyzer() { + // Add a little bit of noise to reduce blockage by speaker protection and DRC. + setNoiseAmplitude(0.02); + } + + double calculatePhaseError(double p1, double p2) { + double diff = p1 - p2; + // Wrap around the circle. + while (diff > M_PI) { + diff -= (2 * M_PI); + } + while (diff < -M_PI) { + diff += (2 * M_PI); + } + return diff; + } + + /** + * @param frameData contains microphone data with sine signal feedback + * @param channelCount + */ + result_code processInputFrame(const float *frameData, int /* channelCount */) override { + result_code result = RESULT_OK; + + float sample = frameData[getInputChannel()]; + mInfiniteRecording.write(sample); + + if (transformSample(sample)) { + // Analyze magnitude and phase on every period. + if (mPhaseOffset != kPhaseInvalid) { + double diff = fabs(calculatePhaseError(mPhaseOffset, mPreviousPhaseOffset)); + if (diff < mPhaseTolerance) { + mMaxMagnitude = std::max(mMagnitude, mMaxMagnitude); + } + mPreviousPhaseOffset = mPhaseOffset; + } + } + return result; + } + + std::string analyze() override { + std::stringstream report; + report << "DataPathAnalyzer ------------------\n"; + report << LOOPBACK_RESULT_TAG "sine.magnitude = " << std::setw(8) + << mMagnitude << "\n"; + report << LOOPBACK_RESULT_TAG "frames.accumulated = " << std::setw(8) + << mFramesAccumulated << "\n"; + report << LOOPBACK_RESULT_TAG "sine.period = " << std::setw(8) + << mSinePeriod << "\n"; + return report.str(); + } + + void reset() override { + BaseSineAnalyzer::reset(); + mPreviousPhaseOffset = 999.0; // Arbitrary high offset to prevent early lock. + mMaxMagnitude = 0.0; + } + + double getMaxMagnitude() { + return mMaxMagnitude; + } + +private: + double mPreviousPhaseOffset = 0.0; + double mPhaseTolerance = 2 * M_PI / 48; + double mMaxMagnitude = 0.0; +}; +#endif // ANALYZER_DATA_PATH_ANALYZER_H diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/analyzer/GlitchAnalyzer.h b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/analyzer/GlitchAnalyzer.h index 7a496f3b..42647440 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/analyzer/GlitchAnalyzer.h +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/analyzer/GlitchAnalyzer.h @@ -14,141 +14,140 @@ * limitations under the License. */ -#ifndef OBOETESTER_GLITCHANALYZER_H -#define OBOETESTER_GLITCHANALYZER_H +#ifndef ANALYZER_GLITCH_ANALYZER_H +#define ANALYZER_GLITCH_ANALYZER_H +#include #include +#include +#include -#include "PseudoRandom.h" -#include "LatencyAnalyzer.h" #include "InfiniteRecording.h" +#include "LatencyAnalyzer.h" +#include "BaseSineAnalyzer.h" +#include "PseudoRandom.h" /** - * Output a steady sinewave and analyze the return signal. + * Output a steady sine wave and analyze the return signal. * * Use a cosine transform to measure the predicted magnitude and relative phase of the * looped back sine wave. Then generate a predicted signal and compare with the actual signal. */ -class GlitchAnalyzer : public LoopbackProcessor { +class GlitchAnalyzer : public BaseSineAnalyzer { public: - GlitchAnalyzer() - : LoopbackProcessor() - , mInfiniteRecording(64 * 1024) {} + GlitchAnalyzer() : BaseSineAnalyzer() {} - int32_t getState() { + int32_t getState() const { return mState; } - float getPeakAmplitude() { + double getPeakAmplitude() const { return mPeakFollower.getLevel(); } - float getTolerance() { - return mTolerance; + double getSineAmplitude() const { + return mMagnitude; } - void setTolerance(float tolerance) { - mTolerance = tolerance; - mScaledTolerance = mMagnitude * mTolerance; + int getSinePeriod() const { + return mSinePeriod; } - void setMagnitude(double magnitude) { - mMagnitude = magnitude; - mScaledTolerance = mMagnitude * mTolerance; - } - - int32_t getGlitchCount() { + int32_t getGlitchCount() const { return mGlitchCount; } - int32_t getStateFrameCount(int state) { + int32_t getGlitchLength() const { + return mGlitchLength; + } + + int32_t getStateFrameCount(int state) const { return mStateFrameCounters[state]; } double getSignalToNoiseDB() { static const double threshold = 1.0e-14; - if (mMeanSquareSignal < threshold || mMeanSquareNoise < threshold) { - return 0.0; + if (mState != STATE_LOCKED + || mMeanSquareSignal < threshold + || mMeanSquareNoise < threshold) { + return -999.0; // error indicator } else { double signalToNoise = mMeanSquareSignal / mMeanSquareNoise; // power ratio double signalToNoiseDB = 10.0 * log(signalToNoise); - if (signalToNoiseDB < MIN_SNRATIO_DB) { - LOGD("ERROR - signal to noise ratio is too low! < %d dB. Adjust volume.", - MIN_SNRATIO_DB); + if (signalToNoiseDB < static_cast(MIN_SNR_DB)) { setResult(ERROR_VOLUME_TOO_LOW); } return signalToNoiseDB; } } - void analyze() override { - LOGD("GlitchAnalyzer ------------------"); - LOGD(LOOPBACK_RESULT_TAG "peak.amplitude = %8f", getPeakAmplitude()); - LOGD(LOOPBACK_RESULT_TAG "sine.magnitude = %8f", mMagnitude); - LOGD(LOOPBACK_RESULT_TAG "rms.noise = %8f", mMeanSquareNoise); - LOGD(LOOPBACK_RESULT_TAG "signal.to.noise.db = %8.2f", getSignalToNoiseDB()); - LOGD(LOOPBACK_RESULT_TAG "frames.accumulated = %8d", mFramesAccumulated); - LOGD(LOOPBACK_RESULT_TAG "sine.period = %8d", mSinePeriod); - LOGD(LOOPBACK_RESULT_TAG "test.state = %8d", mState); - LOGD(LOOPBACK_RESULT_TAG "frame.count = %8d", mFrameCounter); + std::string analyze() override { + std::stringstream report; + report << "GlitchAnalyzer ------------------\n"; + report << LOOPBACK_RESULT_TAG "peak.amplitude = " << std::setw(8) + << getPeakAmplitude() << "\n"; + report << LOOPBACK_RESULT_TAG "sine.magnitude = " << std::setw(8) + << getSineAmplitude() << "\n"; + report << LOOPBACK_RESULT_TAG "rms.noise = " << std::setw(8) + << mMeanSquareNoise << "\n"; + report << LOOPBACK_RESULT_TAG "signal.to.noise.db = " << std::setw(8) + << getSignalToNoiseDB() << "\n"; + report << LOOPBACK_RESULT_TAG "frames.accumulated = " << std::setw(8) + << mFramesAccumulated << "\n"; + report << LOOPBACK_RESULT_TAG "sine.period = " << std::setw(8) + << mSinePeriod << "\n"; + report << LOOPBACK_RESULT_TAG "test.state = " << std::setw(8) + << mState << "\n"; + report << LOOPBACK_RESULT_TAG "frame.count = " << std::setw(8) + << mFrameCounter << "\n"; // Did we ever get a lock? bool gotLock = (mState == STATE_LOCKED) || (mGlitchCount > 0); if (!gotLock) { - LOGD("ERROR - failed to lock on reference sine tone"); + report << "ERROR - failed to lock on reference sine tone.\n"; setResult(ERROR_NO_LOCK); } else { // Only print if meaningful. - LOGD(LOOPBACK_RESULT_TAG "glitch.count = %8d", mGlitchCount); - LOGD(LOOPBACK_RESULT_TAG "max.glitch = %8f", mMaxGlitchDelta); + report << LOOPBACK_RESULT_TAG "glitch.count = " << std::setw(8) + << mGlitchCount << "\n"; + report << LOOPBACK_RESULT_TAG "max.glitch = " << std::setw(8) + << mMaxGlitchDelta << "\n"; if (mGlitchCount > 0) { - LOGD("ERROR - number of glitches > 0"); + report << "ERROR - number of glitches > 0\n"; setResult(ERROR_GLITCHES); } } + return report.str(); } void printStatus() override { - LOGD("st = %d, #gl = %3d,", mState, mGlitchCount); - } - - double calculateMagnitude(double *phasePtr = NULL) { - if (mFramesAccumulated == 0) { - return 0.0; - } - double sinMean = mSinAccumulator / mFramesAccumulated; - double cosMean = mCosAccumulator / mFramesAccumulated; - double magnitude = 2.0 * sqrt( (sinMean * sinMean) + (cosMean * cosMean )); - if( phasePtr != NULL ) - { - double phase = M_PI_2 - atan2( sinMean, cosMean ); - *phasePtr = phase; - } - return magnitude; + ALOGD("st = %d, #gl = %3d,", mState, mGlitchCount); } /** * @param frameData contains microphone data with sine signal feedback * @param channelCount */ - result_code processInputFrame(float *frameData, int channelCount) override { + result_code processInputFrame(const float *frameData, int /* channelCount */) override { result_code result = RESULT_OK; - float sample = frameData[0]; - float peak = mPeakFollower.process(sample); - mInfiniteRecording.write(sample); + float sample = frameData[getInputChannel()]; - // Force a periodic glitch! - if (mForceGlitchDuration > 0) { + // Force a periodic glitch to test the detector! + if (mForceGlitchDurationFrames > 0) { if (mForceGlitchCounter == 0) { - LOGE("%s: force a glitch!!", __func__); - mForceGlitchCounter = getSampleRate(); - } else if (mForceGlitchCounter <= mForceGlitchDuration) { - sample += (sample > 0.0) ? -0.5f : 0.5f; + ALOGE("%s: finish a glitch!!", __func__); + mForceGlitchCounter = kForceGlitchPeriod; + } else if (mForceGlitchCounter <= mForceGlitchDurationFrames) { + // Force an abrupt offset. + sample += (sample > 0.0) ? -kForceGlitchOffset : kForceGlitchOffset; } --mForceGlitchCounter; } + float peak = mPeakFollower.process(sample); + mInfiniteRecording.write(sample); + mStateFrameCounters[mState]++; // count how many frames we are in each state switch (mState) { @@ -159,6 +158,7 @@ public: mDownCounter = IMMUNE_FRAME_COUNT; mInputPhase = 0.0; // prevent spike at start mOutputPhase = 0.0; + resetAccumulator(); } break; @@ -172,28 +172,31 @@ public: case STATE_WAITING_FOR_SIGNAL: if (peak > mThreshold) { mState = STATE_WAITING_FOR_LOCK; - //LOGD("%5d: switch to STATE_WAITING_FOR_LOCK", mFrameCounter); + //ALOGD("%5d: switch to STATE_WAITING_FOR_LOCK", mFrameCounter); resetAccumulator(); } break; case STATE_WAITING_FOR_LOCK: - mSinAccumulator += sample * sinf(mInputPhase); - mCosAccumulator += sample * cosf(mInputPhase); + mSinAccumulator += static_cast(sample) * sinf(mInputPhase); + mCosAccumulator += static_cast(sample) * cosf(mInputPhase); mFramesAccumulated++; // Must be a multiple of the period or the calculation will not be accurate. if (mFramesAccumulated == mSinePeriod * PERIODS_NEEDED_FOR_LOCK) { - double phaseOffset = 0.0; - setMagnitude(calculateMagnitude(&phaseOffset)); -// LOGD("%s() mag = %f, offset = %f, prev = %f", -// __func__, mMagnitude, mPhaseOffset, mPreviousPhaseOffset); - if (mMagnitude > mThreshold) { - if (abs(phaseOffset) < kMaxPhaseError) { - mState = STATE_LOCKED; -// LOGD("%5d: switch to STATE_LOCKED", mFrameCounter); + double magnitude = calculateMagnitudePhase(&mPhaseOffset); + if (mPhaseOffset != kPhaseInvalid) { + setMagnitude(magnitude); + ALOGD("%s() mag = %f, mPhaseOffset = %f", + __func__, magnitude, mPhaseOffset); + if (mMagnitude > mThreshold) { + if (fabs(mPhaseOffset) < kMaxPhaseError) { + mState = STATE_LOCKED; + mConsecutiveBadFrames = 0; +// ALOGD("%5d: switch to STATE_LOCKED", mFrameCounter); + } + // Adjust mInputPhase to match measured phase + mInputPhase += mPhaseOffset; } - // Adjust mInputPhase to match measured phase - mInputPhase += phaseOffset; } resetAccumulator(); } @@ -202,65 +205,72 @@ public: case STATE_LOCKED: { // Predict next sine value - float predicted = sinf(mInputPhase) * mMagnitude; - float diff = predicted - sample; - float absDiff = fabs(diff); + double predicted = sinf(mInputPhase) * mMagnitude; + double diff = predicted - sample; + double absDiff = fabs(diff); mMaxGlitchDelta = std::max(mMaxGlitchDelta, absDiff); - if (absDiff > mScaledTolerance) { - result = ERROR_GLITCHES; - onGlitchStart(); -// LOGI("diff glitch detected, absDiff = %g", absDiff); - } else { + if (absDiff > mScaledTolerance) { // bad frame + mConsecutiveBadFrames++; + mConsecutiveGoodFrames = 0; + LOGI("diff glitch frame #%d detected, absDiff = %g > %g", + mConsecutiveBadFrames, absDiff, mScaledTolerance); + if (mConsecutiveBadFrames > 0) { + result = ERROR_GLITCHES; + onGlitchStart(); + } + resetAccumulator(); + } else { // good frame + mConsecutiveBadFrames = 0; + mConsecutiveGoodFrames++; + mSumSquareSignal += predicted * predicted; mSumSquareNoise += diff * diff; + // Track incoming signal and slowly adjust magnitude to account // for drift in the DRC or AGC. - mSinAccumulator += sample * sinf(mInputPhase); - mCosAccumulator += sample * cosf(mInputPhase); - mFramesAccumulated++; // Must be a multiple of the period or the calculation will not be accurate. - if (mFramesAccumulated == mSinePeriod) { - const double coefficient = 0.1; - double phaseOffset = 0.0; - double magnitude = calculateMagnitude(&phaseOffset); - // One pole averaging filter. - setMagnitude((mMagnitude * (1.0 - coefficient)) + (magnitude * coefficient)); + if (transformSample(sample)) { + // Adjust phase to account for sample rate drift. + mInputPhase += mPhaseOffset; mMeanSquareNoise = mSumSquareNoise * mInverseSinePeriod; mMeanSquareSignal = mSumSquareSignal * mInverseSinePeriod; - resetAccumulator(); + mSumSquareNoise = 0.0; + mSumSquareSignal = 0.0; - if (abs(phaseOffset) > kMaxPhaseError) { + if (fabs(mPhaseOffset) > kMaxPhaseError) { result = ERROR_GLITCHES; onGlitchStart(); - LOGD("phase glitch detected, phaseOffset = %g", phaseOffset); + ALOGD("phase glitch detected, phaseOffset = %g", mPhaseOffset); } else if (mMagnitude < mThreshold) { result = ERROR_GLITCHES; onGlitchStart(); - LOGD("magnitude glitch detected, mMagnitude = %g", mMagnitude); + ALOGD("magnitude glitch detected, mMagnitude = %g", mMagnitude); } } } - incrementInputPhase(); } break; case STATE_GLITCHING: { // Predict next sine value - mGlitchLength++; - float predicted = sinf(mInputPhase) * mMagnitude; - float diff = predicted - sample; - float absDiff = fabs(diff); + double predicted = sinf(mInputPhase) * mMagnitude; + double diff = predicted - sample; + double absDiff = fabs(diff); mMaxGlitchDelta = std::max(mMaxGlitchDelta, absDiff); - if (absDiff < mScaledTolerance) { // close enough? - // If we get a full sine period of non-glitch samples in a row then consider the glitch over. - // We don't want to just consider a zero crossing the end of a glitch. - if (mNonGlitchCount++ > mSinePeriod) { - onGlitchEnd(); + if (absDiff > mScaledTolerance) { // bad frame + mConsecutiveBadFrames++; + mConsecutiveGoodFrames = 0; + mGlitchLength++; + if (mGlitchLength > maxMeasurableGlitchLength()) { + onGlitchTerminated(); } - } else { - mNonGlitchCount = 0; - if (mGlitchLength > (4 * mSinePeriod)) { - relock(); + } else { // good frame + mConsecutiveBadFrames = 0; + mConsecutiveGoodFrames++; + // If we get a full sine period of good samples in a row then consider the glitch over. + // We don't want to just consider a zero crossing the end of a glitch. + if (mConsecutiveGoodFrames > mSinePeriod) { + onGlitchEnd(); } } incrementInputPhase(); @@ -275,96 +285,80 @@ public: return result; } - // advance and wrap phase - void incrementInputPhase() { - mInputPhase += mPhaseIncrement; - if (mInputPhase > M_PI) { - mInputPhase -= (2.0 * M_PI); - } - } + int maxMeasurableGlitchLength() const { return 2 * mSinePeriod; } - // advance and wrap phase - void incrementOutputPhase() { - mOutputPhase += mPhaseIncrement; - if (mOutputPhase > M_PI) { - mOutputPhase -= (2.0 * M_PI); - } + bool isOutputEnabled() override { return mState != STATE_IDLE; } + + void onGlitchStart() { + mState = STATE_GLITCHING; + mGlitchLength = 1; + mLastGlitchPosition = mInfiniteRecording.getTotalWritten(); + ALOGD("%5d: STARTED a glitch # %d, pos = %5d", + mFrameCounter, mGlitchCount, (int)mLastGlitchPosition); + ALOGD("glitch mSinePeriod = %d", mSinePeriod); } /** - * @param frameData upon return, contains the reference sine wave - * @param channelCount + * Give up waiting for a glitch to end and try to resync. */ - result_code processOutputFrame(float *frameData, int channelCount) override { - float output = 0.0f; - // Output sine wave so we can measure it. - if (mState != STATE_IDLE) { - float sinOut = sinf(mOutputPhase); - incrementOutputPhase(); - output = (sinOut * mOutputAmplitude) - + (mWhiteNoise.nextRandomDouble() * kNoiseAmplitude); - // LOGD("%5d: sin(%f) = %f, %f", i, mPhase, sinOut, mPhaseIncrement); - } - frameData[0] = output; - for (int i = 1; i < channelCount; i++) { - frameData[i] = 0.0f; - } - return RESULT_OK; - } - - void onGlitchStart() { + void onGlitchTerminated() { mGlitchCount++; -// LOGD("%5d: STARTED a glitch # %d", mFrameCounter, mGlitchCount); - mState = STATE_GLITCHING; - mGlitchLength = 1; - mNonGlitchCount = 0; - mLastGlitchPosition = mInfiniteRecording.getTotalWritten(); + ALOGD("%5d: TERMINATED a glitch # %d, length = %d", mFrameCounter, mGlitchCount, mGlitchLength); + // We don't know how long the glitch really is so set the length to -1. + mGlitchLength = -1; + mState = STATE_WAITING_FOR_LOCK; + resetAccumulator(); } void onGlitchEnd() { -// LOGD("%5d: ENDED a glitch # %d, length = %d", mFrameCounter, mGlitchCount, mGlitchLength); + mGlitchCount++; + ALOGD("%5d: ENDED a glitch # %d, length = %d", mFrameCounter, mGlitchCount, mGlitchLength); mState = STATE_LOCKED; resetAccumulator(); } // reset the sine wave detector - void resetAccumulator() { - mFramesAccumulated = 0; - mSinAccumulator = 0.0; - mCosAccumulator = 0.0; - mSumSquareSignal = 0.0; - mSumSquareNoise = 0.0; - } - - void relock() { -// LOGD("relock: %d because of a very long %d glitch", mFrameCounter, mGlitchLength); - mState = STATE_WAITING_FOR_LOCK; - resetAccumulator(); + void resetAccumulator() override { + BaseSineAnalyzer::resetAccumulator(); } void reset() override { - LoopbackProcessor::reset(); + BaseSineAnalyzer::reset(); mState = STATE_IDLE; mDownCounter = IDLE_FRAME_COUNT; - resetAccumulator(); } - void onStartTest() override { - LoopbackProcessor::onStartTest(); - mSinePeriod = getSampleRate() / kTargetGlitchFrequency; - mOutputPhase = 0.0f; - mInverseSinePeriod = 1.0 / mSinePeriod; - mPhaseIncrement = 2.0 * M_PI * mInverseSinePeriod; + void prepareToTest() override { + BaseSineAnalyzer::prepareToTest(); mGlitchCount = 0; + mGlitchLength = 0; mMaxGlitchDelta = 0.0; for (int i = 0; i < NUM_STATES; i++) { mStateFrameCounters[i] = 0; } } - int32_t getLastGlitch(float *buffer, int32_t length) { - return mInfiniteRecording.readFrom(buffer, mLastGlitchPosition - 32, length); + const int margin = mSinePeriod; + int32_t numSamples = mInfiniteRecording.readFrom(buffer, + mLastGlitchPosition - margin, + length); + ALOGD("%s: glitch at %d, edge = %7.4f, %7.4f, %7.4f", + __func__, (int)mLastGlitchPosition, + buffer[margin - 1], buffer[margin], buffer[margin+1]); + return numSamples; + } + + int32_t getRecentSamples(float *buffer, int32_t length) { + int firstSample = mInfiniteRecording.getTotalWritten() - length; + int32_t numSamples = mInfiniteRecording.readFrom(buffer, + firstSample, + length); + return numSamples; + } + + void setForcedGlitchDuration(int frames) { + mForceGlitchDurationFrames = frames; } private: @@ -372,51 +366,42 @@ private: // These must match the values in GlitchActivity.java enum sine_state_t { STATE_IDLE, // beginning - STATE_IMMUNE, // ignoring input, waiting fo HW to settle + STATE_IMMUNE, // ignoring input, waiting for HW to settle STATE_WAITING_FOR_SIGNAL, // looking for a loud signal STATE_WAITING_FOR_LOCK, // trying to lock onto the phase of the sine STATE_LOCKED, // locked on the sine wave, looking for glitches - STATE_GLITCHING, // locked on the sine wave but glitching + STATE_GLITCHING, // locked on the sine wave but glitching NUM_STATES }; enum constants { // Arbitrary durations, assuming 48000 Hz - IDLE_FRAME_COUNT = 48 * 100, + IDLE_FRAME_COUNT = 48 * 100, IMMUNE_FRAME_COUNT = 48 * 100, PERIODS_NEEDED_FOR_LOCK = 8, - MIN_SNRATIO_DB = 65 + MIN_SNR_DB = 65 }; - static constexpr float kNoiseAmplitude = 0.00; // Used to experiment with warbling caused by DRC. - static constexpr int kTargetGlitchFrequency = 607; static constexpr double kMaxPhaseError = M_PI * 0.05; - float mTolerance = 0.10; // scaled from 0.0 to 1.0 double mThreshold = 0.005; - int mSinePeriod = 1; // this will be set before use - double mInverseSinePeriod = 1.0; int32_t mStateFrameCounters[NUM_STATES]; + sine_state_t mState = STATE_IDLE; + int64_t mLastGlitchPosition; - double mPhaseIncrement = 0.0; - double mInputPhase = 0.0; - double mOutputPhase = 0.0; - double mMagnitude = 0.0; - int32_t mFramesAccumulated = 0; - double mSinAccumulator = 0.0; - double mCosAccumulator = 0.0; - float mMaxGlitchDelta = 0.0f; + double mMaxGlitchDelta = 0.0; int32_t mGlitchCount = 0; - int32_t mNonGlitchCount = 0; + int32_t mConsecutiveBadFrames = 0; + int32_t mConsecutiveGoodFrames = 0; int32_t mGlitchLength = 0; - float mScaledTolerance = 0.0; int mDownCounter = IDLE_FRAME_COUNT; int32_t mFrameCounter = 0; - float mOutputAmplitude = 0.75; - int32_t mForceGlitchDuration = 0; // if > 0 then force a glitch for debugging - int32_t mForceGlitchCounter = 4 * 48000; // count down and trigger at zero + int32_t mForceGlitchDurationFrames = 0; // if > 0 then force a glitch for debugging + static constexpr int32_t kForceGlitchPeriod = 2 * 48000; // How often we glitch + static constexpr float kForceGlitchOffset = 0.20f; + int32_t mForceGlitchCounter = kForceGlitchPeriod; // count down and trigger at zero // measure background noise continuously as a deviation from the expected signal double mSumSquareSignal = 0.0; @@ -425,14 +410,7 @@ private: double mMeanSquareNoise = 0.0; PeakDetector mPeakFollower; - - PseudoRandom mWhiteNoise; - - sine_state_t mState = STATE_IDLE; - - InfiniteRecording mInfiniteRecording; - int64_t mLastGlitchPosition; }; -#endif //OBOETESTER_GLITCHANALYZER_H +#endif //ANALYZER_GLITCH_ANALYZER_H diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/analyzer/InfiniteRecording.h b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/analyzer/InfiniteRecording.h index c02c0025..c12348c0 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/analyzer/InfiniteRecording.h +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/analyzer/InfiniteRecording.h @@ -34,6 +34,7 @@ public: int32_t readFrom(T *buffer, size_t position, size_t count) { const size_t maxPosition = mWritten.load(); position = std::min(position, maxPosition); + size_t numToRead = std::min(count, mMaxSamples); numToRead = std::min(numToRead, maxPosition - position); if (numToRead == 0) return 0; @@ -61,7 +62,7 @@ public: private: std::unique_ptr mData; - std::atomic mWritten{0}; - const size_t mMaxSamples; + std::atomic mWritten{0}; + const size_t mMaxSamples; }; #endif //OBOETESTER_INFINITE_RECORDING_H diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/analyzer/LatencyAnalyzer.h b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/analyzer/LatencyAnalyzer.h index d0d1aa81..165ba140 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/analyzer/LatencyAnalyzer.h +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/analyzer/LatencyAnalyzer.h @@ -25,45 +25,61 @@ #include #include #include +#include +#include #include #include +#include #include #include #include #include -#include "RandomPulseGenerator.h" #include "PeakDetector.h" #include "PseudoRandom.h" +#include "RandomPulseGenerator.h" + +// This is used when the code is in not in Android. +#ifndef ALOGD +#define ALOGD LOGD +#define ALOGE LOGE +#define ALOGW LOGW +#endif #define LOOPBACK_RESULT_TAG "RESULT: " -static constexpr int32_t kDefaultSampleRate = 48000; -static constexpr int32_t kMillisPerSecond = 1000; -static constexpr int32_t kMaxLatencyMillis = 700; // arbitrary and generous -static constexpr double kMinimumConfidence = 0.2; +// Enable or disable the optimized latency calculation. +#define USE_FAST_LATENCY_CALCULATION 1 -typedef struct LatencyReport_s { - int32_t latencyInFrames = 0.0; - double confidence = 0.0; +static constexpr int32_t kDefaultSampleRate = 48000; +static constexpr int32_t kMillisPerSecond = 1000; // by definition +static constexpr int32_t kMaxLatencyMillis = 1000; // arbitrary and generous + +struct LatencyReport { + int32_t latencyInFrames = 0; + double correlation = 0.0; void reset() { latencyInFrames = 0; - confidence = 0.0; + correlation = 0.0; } -} LatencyReport; +}; -// Calculate a normalized cross correlation. -static double calculateNormalizedCorrelation(const float *a, +/** + * Calculate a normalized cross correlation. + * @return value between -1.0 and 1.0 + */ + +static float calculateNormalizedCorrelation(const float *a, const float *b, - int windowSize) -{ - double correlation = 0.0; - double sumProducts = 0.0; - double sumSquares = 0.0; + int windowSize, + int stride) { + float correlation = 0.0; + float sumProducts = 0.0; + float sumSquares = 0.0; // Correlate a against b. - for (int i = 0; i < windowSize; i++) { + for (int i = 0; i < windowSize; i += stride) { float s1 = a[i]; float s2 = b[i]; // Use a normalized cross-correlation. @@ -72,7 +88,7 @@ static double calculateNormalizedCorrelation(const float *a, } if (sumSquares >= 1.0e-9) { - correlation = (float) (2.0 * sumProducts / sumSquares); + correlation = 2.0 * sumProducts / sumSquares; } return correlation; } @@ -80,7 +96,7 @@ static double calculateNormalizedCorrelation(const float *a, static double calculateRootMeanSquare(float *data, int32_t numSamples) { double sum = 0.0; for (int32_t i = 0; i < numSamples; i++) { - float sample = data[i]; + double sample = data[i]; sum += sample * sample; } return sqrt(sum / numSamples); @@ -88,24 +104,20 @@ static double calculateRootMeanSquare(float *data, int32_t numSamples) { /** * Monophonic recording with processing. + * Samples are stored as floats internally. */ class AudioRecording { public: - AudioRecording() { - } - ~AudioRecording() { - delete[] mData; - } void allocate(int maxFrames) { - delete[] mData; - mData = new float[maxFrames]; + mData = std::make_unique(maxFrames); mMaxFrames = maxFrames; + mFrameCounter = 0; } // Write SHORT data from the first channel. - int32_t write(int16_t *inputData, int32_t inputChannelCount, int32_t numFrames) { + int32_t write(const int16_t *inputData, int32_t inputChannelCount, int32_t numFrames) { // stop at end of buffer if ((mFrameCounter + numFrames) > mMaxFrames) { numFrames = mMaxFrames - mFrameCounter; @@ -117,7 +129,7 @@ public: } // Write FLOAT data from the first channel. - int32_t write(float *inputData, int32_t inputChannelCount, int32_t numFrames) { + int32_t write(const float *inputData, int32_t inputChannelCount, int32_t numFrames) { // stop at end of buffer if ((mFrameCounter + numFrames) > mMaxFrames) { numFrames = mMaxFrames - mFrameCounter; @@ -128,35 +140,37 @@ public: return numFrames; } - // Write FLOAT data from the first channel. + // Write single FLOAT value. int32_t write(float sample) { // stop at end of buffer if (mFrameCounter < mMaxFrames) { mData[mFrameCounter++] = sample; + return 1; } - return 1; + return 0; } void clear() { mFrameCounter = 0; } - int32_t size() { + + int32_t size() const { return mFrameCounter; } - bool isFull() { + bool isFull() const { return mFrameCounter >= mMaxFrames; } - float *getData() { - return mData; + float *getData() const { + return mData.get(); } void setSampleRate(int32_t sampleRate) { mSampleRate = sampleRate; } - int32_t getSampleRate() { + int32_t getSampleRate() const { return mSampleRate; } @@ -164,9 +178,24 @@ public: * Square the samples so they are all positive and so the peaks are emphasized. */ void square() { + float *x = mData.get(); for (int i = 0; i < mFrameCounter; i++) { - const float sample = mData[i]; - mData[i] = sample * sample; + x[i] *= x[i]; + } + } + + // Envelope follower that rides over the peak values. + void detectPeaks(float decay) { + float level = 0.0f; + float *x = mData.get(); + for (int i = 0; i < mFrameCounter; i++) { + level *= decay; // exponential decay + float input = fabs(x[i]); + // never fall below the input signal + if (input > level) { + level = input; + } + x[i] = level; // write result back into the array } } @@ -179,7 +208,7 @@ public: float normalize(float target) { float maxValue = 1.0e-9f; for (int i = 0; i < mFrameCounter; i++) { - maxValue = std::max(maxValue, abs(mData[i])); + maxValue = std::max(maxValue, fabsf(mData[i])); } float gain = target / maxValue; for (int i = 0; i < mFrameCounter; i++) { @@ -189,62 +218,127 @@ public: } private: - float *mData = nullptr; + std::unique_ptr mData; int32_t mFrameCounter = 0; int32_t mMaxFrames = 0; int32_t mSampleRate = kDefaultSampleRate; // common default }; -static int measureLatencyFromPulse(AudioRecording &recorded, - AudioRecording &pulse, - int32_t framesPerEncodedBit, - LatencyReport *report) { +/** + * Find latency using cross correlation in window of the recorded audio. + * The stride is used to skip over samples and reduce the CPU load. + */ +static int measureLatencyFromPulsePartial(AudioRecording &recorded, + int32_t recordedOffset, + int32_t recordedWindowSize, + AudioRecording &pulse, + LatencyReport *report, + int32_t stride) { + report->reset(); - report->latencyInFrames = 0; - report->confidence = 0.0; + if (recordedOffset + recordedWindowSize + pulse.size() > recorded.size()) { + ALOGE("%s() tried to correlate past end of recording, recordedOffset = %d frames\n", + __func__, recordedOffset); + return -3; + } - int numCorrelations = recorded.size() - pulse.size(); + int32_t numCorrelations = recordedWindowSize / stride; if (numCorrelations < 10) { - LOGE("%s() recording too small = %d frames", __func__, recorded.size()); + ALOGE("%s() recording too small = %d frames, numCorrelations = %d\n", + __func__, recorded.size(), numCorrelations); return -1; } std::unique_ptr correlations= std::make_unique(numCorrelations); // Correlate pulse against the recorded data. - for (int i = 0; i < numCorrelations; i++) { - float correlation = (float) calculateNormalizedCorrelation(&recorded.getData()[i], - &pulse.getData()[0], - pulse.size()); + for (int32_t i = 0; i < numCorrelations; i++) { + const int32_t index = (i * stride) + recordedOffset; + float correlation = calculateNormalizedCorrelation(&recorded.getData()[index], + &pulse.getData()[0], + pulse.size(), + stride); correlations[i] = correlation; } - // Find highest peak in correlation array. float peakCorrelation = 0.0; - int peakIndex = -1; - for (int i = 0; i < numCorrelations; i++) { - float value = abs(correlations[i]); + int32_t peakIndex = -1; + for (int32_t i = 0; i < numCorrelations; i++) { + float value = fabsf(correlations[i]); if (value > peakCorrelation) { peakCorrelation = value; peakIndex = i; } } if (peakIndex < 0) { - LOGE("%s() no signal for correlation", __func__); + ALOGE("%s() no signal for correlation\n", __func__); return -2; } +#if 0 + // Dump correlation data for charting. + else { + const int32_t margin = 50; + int32_t startIndex = std::max(0, peakIndex - margin); + int32_t endIndex = std::min(numCorrelations - 1, peakIndex + margin); + for (int32_t index = startIndex; index < endIndex; index++) { + ALOGD("Correlation, %d, %f", index, correlations[index]); + } + } +#endif - report->latencyInFrames = peakIndex; - report->confidence = peakCorrelation; + report->latencyInFrames = recordedOffset + (peakIndex * stride); + report->correlation = peakCorrelation; return 0; } +#if USE_FAST_LATENCY_CALCULATION +static int measureLatencyFromPulse(AudioRecording &recorded, + AudioRecording &pulse, + LatencyReport *report) { + const int32_t coarseStride = 16; + const int32_t fineWindowSize = coarseStride * 8; + const int32_t fineStride = 1; + LatencyReport courseReport; + courseReport.reset(); + // Do a rough search, skipping over most of the samples. + int result = measureLatencyFromPulsePartial(recorded, + 0, // recordedOffset, + recorded.size() - pulse.size(), + pulse, + &courseReport, + coarseStride); + if (result != 0) { + return result; + } + // Now do a fine resolution search near the coarse latency result. + int32_t recordedOffset = std::max(0, courseReport.latencyInFrames - (fineWindowSize / 2)); + result = measureLatencyFromPulsePartial(recorded, + recordedOffset, + fineWindowSize, + pulse, + report, + fineStride ); + return result; +} +#else +// TODO - When we are confident of the new code we can remove this old code. +static int measureLatencyFromPulse(AudioRecording &recorded, + AudioRecording &pulse, + LatencyReport *report) { + return measureLatencyFromPulsePartial(recorded, + 0, + recorded.size() - pulse.size(), + pulse, + report, + 1 ); +} +#endif + // ==================================================================================== class LoopbackProcessor { public: virtual ~LoopbackProcessor() = default; - // Note that these values must match the switch in RoundTripLatencyActivity.h enum result_code { RESULT_OK = 0, ERROR_NOISY = -99, @@ -256,7 +350,7 @@ public: ERROR_NO_LOCK }; - virtual void onStartTest() { + virtual void prepareToTest() { reset(); } @@ -265,11 +359,11 @@ public: mResetCount++; } - virtual result_code processInputFrame(float *frameData, int channelCount) = 0; + virtual result_code processInputFrame(const float *frameData, int channelCount) = 0; virtual result_code processOutputFrame(float *frameData, int channelCount) = 0; - void process(float *inputData, int inputChannelCount, int numInputFrames, - float *outputData, int outputChannelCount, int numOutputFrames) { + void process(const float *inputData, int inputChannelCount, int numInputFrames, + float *outputData, int outputChannelCount, int numOutputFrames) { int numBoth = std::min(numInputFrames, numOutputFrames); // Process one frame at a time. for (int i = 0; i < numBoth; i++) { @@ -290,7 +384,7 @@ public: } } - virtual void analyze() = 0; + virtual std::string analyze() = 0; virtual void printStatus() {}; @@ -320,11 +414,11 @@ public: mSampleRate = sampleRate; } - int32_t getSampleRate() { + int32_t getSampleRate() const { return mSampleRate; } - int32_t getResetCount() { + int32_t getResetCount() const { return mResetCount; } @@ -334,10 +428,41 @@ public: reset(); } + /** + * Some analyzers may only look at one channel. + * You can optionally specify that channel here. + * + * @param inputChannel + */ + void setInputChannel(int inputChannel) { + mInputChannel = inputChannel; + } + + int getInputChannel() const { + return mInputChannel; + } + + /** + * Some analyzers may only generate one channel. + * You can optionally specify that channel here. + * + * @param outputChannel + */ + void setOutputChannel(int outputChannel) { + mOutputChannel = outputChannel; + } + + int getOutputChannel() const { + return mOutputChannel; + } + protected: int32_t mResetCount = 0; private: + + int32_t mInputChannel = 0; + int32_t mOutputChannel = 0; int32_t mSampleRate = kDefaultSampleRate; int32_t mResult = 0; }; @@ -348,19 +473,44 @@ public: LatencyAnalyzer() : LoopbackProcessor() {} virtual ~LatencyAnalyzer() = default; - virtual int32_t getProgress() = 0; + /** + * Call this after the constructor because it calls other virtual methods. + */ + virtual void setup() = 0; - virtual int getState() = 0; + virtual int32_t getProgress() const = 0; + + virtual int getState() const = 0; // @return latency in frames - virtual int32_t getMeasuredLatency() = 0; + virtual int32_t getMeasuredLatency() const = 0; - virtual double getMeasuredConfidence() = 0; + /** + * This is an overall confidence in the latency result based on correlation, SNR, etc. + * @return probability value between 0.0 and 1.0 + */ + double getMeasuredConfidence() const { + // Limit the ratio and prevent divide-by-zero. + double noiseSignalRatio = getSignalRMS() <= getBackgroundRMS() + ? 1.0 : getBackgroundRMS() / getSignalRMS(); + // Prevent high background noise and low signals from generating false matches. + double adjustedConfidence = getMeasuredCorrelation() - noiseSignalRatio; + return std::max(0.0, adjustedConfidence); + } - virtual double getBackgroundRMS() = 0; + /** + * Cross correlation value for the noise pulse against + * the corresponding position in the normalized recording. + * + * @return value between -1.0 and 1.0 + */ + virtual double getMeasuredCorrelation() const = 0; - virtual double getSignalRMS() = 0; + virtual double getBackgroundRMS() const = 0; + virtual double getSignalRMS() const = 0; + + virtual bool hasEnoughData() const = 0; }; // ==================================================================================== @@ -374,27 +524,15 @@ public: class PulseLatencyAnalyzer : public LatencyAnalyzer { public: - PulseLatencyAnalyzer() : LatencyAnalyzer() { + void setup() override { + int32_t pulseLength = calculatePulseLength(); int32_t maxLatencyFrames = getSampleRate() * kMaxLatencyMillis / kMillisPerSecond; - int32_t numPulseBits = getSampleRate() * kPulseLengthMillis - / (kFramesPerEncodedBit * kMillisPerSecond); - int32_t pulseLength = numPulseBits * kFramesPerEncodedBit; mFramesToRecord = pulseLength + maxLatencyFrames; - LOGD("PulseLatencyAnalyzer: allocate recording with %d frames", mFramesToRecord); mAudioRecording.allocate(mFramesToRecord); mAudioRecording.setSampleRate(getSampleRate()); - generateRandomPulse(pulseLength); } - void generateRandomPulse(int32_t pulseLength) { - mPulse.allocate(pulseLength); - RandomPulseGenerator pulser(kFramesPerEncodedBit); - for (int i = 0; i < pulseLength; i++) { - mPulse.write(pulser.nextFloat()); - } - } - - int getState() override { + int getState() const override { return mState; } @@ -405,7 +543,8 @@ public: void reset() override { LoopbackProcessor::reset(); - mDownCounter = getSampleRate() / 2; + mState = STATE_MEASURE_BACKGROUND; + mDownCounter = (int32_t) (getSampleRate() * kBackgroundMeasurementLengthSeconds); mLoopCounter = 0; mPulseCursor = 0; @@ -414,13 +553,12 @@ public: mBackgroundRMS = 0.0f; mSignalRMS = 0.0f; - LOGD("state reset to STATE_MEASURE_BACKGROUND"); - mState = STATE_MEASURE_BACKGROUND; + generatePulseRecording(calculatePulseLength()); mAudioRecording.clear(); mLatencyReport.reset(); } - bool hasEnoughData() { + bool hasEnoughData() const override { return mAudioRecording.isFull(); } @@ -428,96 +566,103 @@ public: return mState == STATE_DONE; } - int32_t getProgress() override { + int32_t getProgress() const override { return mAudioRecording.size(); } - void analyze() override { - LOGD("PulseLatencyAnalyzer ---------------"); - LOGD(LOOPBACK_RESULT_TAG "test.state = %8d", mState); - LOGD(LOOPBACK_RESULT_TAG "test.state.name = %8s", convertStateToText(mState)); - LOGD(LOOPBACK_RESULT_TAG "background.rms = %8f", mBackgroundRMS); + std::string analyze() override { + std::stringstream report; + report << "PulseLatencyAnalyzer ---------------\n"; + report << LOOPBACK_RESULT_TAG "test.state = " + << std::setw(8) << mState << "\n"; + report << LOOPBACK_RESULT_TAG "test.state.name = " + << convertStateToText(mState) << "\n"; + report << LOOPBACK_RESULT_TAG "background.rms = " + << std::setw(8) << mBackgroundRMS << "\n"; int32_t newResult = RESULT_OK; if (mState != STATE_GOT_DATA) { - LOGD("WARNING - Bad state. Check volume on device."); + report << "WARNING - Bad state. Check volume on device.\n"; // setResult(ERROR_INVALID_STATE); } else { - LOGD("Please wait several seconds for cross-correlation to complete."); float gain = mAudioRecording.normalize(1.0f); - measureLatencyFromPulse(mAudioRecording, - mPulse, - kFramesPerEncodedBit, - &mLatencyReport); + measureLatency(); - if (mLatencyReport.confidence < kMinimumConfidence) { - LOGD(" ERROR - confidence too low!"); + // Calculate signalRMS even if it is bogus. + // Also it may be used in the confidence calculation below. + mSignalRMS = calculateRootMeanSquare( + &mAudioRecording.getData()[mLatencyReport.latencyInFrames], mPulse.size()) + / gain; + if (getMeasuredConfidence() < getMinimumConfidence()) { + report << " ERROR - confidence too low!"; newResult = ERROR_CONFIDENCE; - } else { - mSignalRMS = calculateRootMeanSquare( - &mAudioRecording.getData()[mLatencyReport.latencyInFrames], mPulse.size()) - / gain; } -#if OBOE_ENABLE_LOGGING + double latencyMillis = kMillisPerSecond * (double) mLatencyReport.latencyInFrames / getSampleRate(); -#endif - LOGD(LOOPBACK_RESULT_TAG "latency.frames = %8d", - mLatencyReport.latencyInFrames); - LOGD(LOOPBACK_RESULT_TAG "latency.msec = %8.2f", - latencyMillis); - LOGD(LOOPBACK_RESULT_TAG "latency.confidence = %8.6f", - mLatencyReport.confidence); + report << LOOPBACK_RESULT_TAG "latency.frames = " << std::setw(8) + << mLatencyReport.latencyInFrames << "\n"; + report << LOOPBACK_RESULT_TAG "latency.msec = " << std::setw(8) + << latencyMillis << "\n"; + report << LOOPBACK_RESULT_TAG "latency.confidence = " << std::setw(8) + << getMeasuredConfidence() << "\n"; + report << LOOPBACK_RESULT_TAG "latency.correlation = " << std::setw(8) + << getMeasuredCorrelation() << "\n"; } mState = STATE_DONE; if (getResult() == RESULT_OK) { setResult(newResult); } + + return report.str(); } - int32_t getMeasuredLatency() override { + int32_t getMeasuredLatency() const override { return mLatencyReport.latencyInFrames; } - double getMeasuredConfidence() override { - return mLatencyReport.confidence; + double getMeasuredCorrelation() const override { + return mLatencyReport.correlation; } - double getBackgroundRMS() override { + double getBackgroundRMS() const override { return mBackgroundRMS; } - double getSignalRMS() override { + double getSignalRMS() const override { return mSignalRMS; } - void printStatus() override { - LOGD("st = %d", mState); + bool isRecordingComplete() { + return mState == STATE_GOT_DATA; } - result_code processInputFrame(float *frameData, int channelCount) override { + void printStatus() override { + ALOGD("latency: st = %d = %s", mState, convertStateToText(mState)); + } + + result_code processInputFrame(const float *frameData, int /* channelCount */) override { echo_state nextState = mState; mLoopCounter++; + float input = frameData[0]; switch (mState) { case STATE_MEASURE_BACKGROUND: // Measure background RMS on channel 0 - mBackgroundSumSquare += frameData[0] * frameData[0]; + mBackgroundSumSquare += static_cast(input) * input; mBackgroundSumCount++; mDownCounter--; if (mDownCounter <= 0) { mBackgroundRMS = sqrtf(mBackgroundSumSquare / mBackgroundSumCount); nextState = STATE_IN_PULSE; mPulseCursor = 0; - LOGD("LatencyAnalyzer state => STATE_SENDING_PULSE"); } break; case STATE_IN_PULSE: // Record input until the mAudioRecording is full. - mAudioRecording.write(frameData, channelCount, 1); + mAudioRecording.write(input); if (hasEnoughData()) { - LOGD("LatencyAnalyzer state => STATE_GOT_DATA"); nextState = STATE_GOT_DATA; } break; @@ -560,6 +705,27 @@ public: return RESULT_OK; } +protected: + + virtual int32_t calculatePulseLength() const = 0; + + virtual void generatePulseRecording(int32_t pulseLength) = 0; + + virtual void measureLatency() = 0; + + virtual double getMinimumConfidence() const { + return 0.5; + } + + AudioRecording mPulse; + AudioRecording mAudioRecording; // contains only the input after starting the pulse + LatencyReport mLatencyReport; + + static constexpr int32_t kPulseLengthMillis = 500; + float mPulseAmplitude = 0.5f; + double mBackgroundRMS = 0.0; + double mSignalRMS = 0.0; + private: enum echo_state { @@ -570,42 +736,127 @@ private: }; const char *convertStateToText(echo_state state) { - const char *result = "Unknown"; - switch(state) { + switch (state) { case STATE_MEASURE_BACKGROUND: - result = "INIT"; - break; + return "INIT"; case STATE_IN_PULSE: - result = "PULSE"; - break; + return "PULSE"; case STATE_GOT_DATA: - result = "GOT_DATA"; - break; + return "GOT_DATA"; case STATE_DONE: - result = "DONE"; - break; + return "DONE"; } - return result; + return "UNKNOWN"; } int32_t mDownCounter = 500; int32_t mLoopCounter = 0; echo_state mState = STATE_MEASURE_BACKGROUND; - static constexpr int32_t kFramesPerEncodedBit = 8; // multiple of 2 - static constexpr int32_t kPulseLengthMillis = 500; + static constexpr double kBackgroundMeasurementLengthSeconds = 0.5; - AudioRecording mPulse; int32_t mPulseCursor = 0; - float mBackgroundSumSquare = 0.0f; + double mBackgroundSumSquare = 0.0; int32_t mBackgroundSumCount = 0; - float mBackgroundRMS = 0.0f; - float mSignalRMS = 0.0f; int32_t mFramesToRecord = 0; - AudioRecording mAudioRecording; // contains only the input after starting the pulse - LatencyReport mLatencyReport; +}; + +/** + * This algorithm uses a series of random bits encoded using the + * Manchester encoder. It works well for wired loopback but not very well for + * through the air loopback. + */ +class EncodedRandomLatencyAnalyzer : public PulseLatencyAnalyzer { + +protected: + + int32_t calculatePulseLength() const override { + // Calculate integer number of bits. + int32_t numPulseBits = getSampleRate() * kPulseLengthMillis + / (kFramesPerEncodedBit * kMillisPerSecond); + return numPulseBits * kFramesPerEncodedBit; + } + + void generatePulseRecording(int32_t pulseLength) override { + mPulse.allocate(pulseLength); + RandomPulseGenerator pulser(kFramesPerEncodedBit); + for (int i = 0; i < pulseLength; i++) { + mPulse.write(pulser.nextFloat() * mPulseAmplitude); + } + } + + double getMinimumConfidence() const override { + return 0.2; + } + + void measureLatency() override { + measureLatencyFromPulse(mAudioRecording, + mPulse, + &mLatencyReport); + } + +private: + static constexpr int32_t kFramesPerEncodedBit = 8; // multiple of 2 +}; + +/** + * This algorithm uses White Noise sent in a short burst pattern. + * The original signal and the recorded signal are then run through + * an envelope follower to convert the fine detail into more of + * a rectangular block before the correlation phase. + */ +class WhiteNoiseLatencyAnalyzer : public PulseLatencyAnalyzer { + +protected: + + int32_t calculatePulseLength() const override { + return getSampleRate() * kPulseLengthMillis / kMillisPerSecond; + } + + void generatePulseRecording(int32_t pulseLength) override { + mPulse.allocate(pulseLength); + // Turn the noise on and off to sharpen the correlation peak. + // Use more zeros than ones so that the correlation will be less than 0.5 even when there + // is a strong background noise. + int8_t pattern[] = {1, 0, 0, + 1, 1, 0, 0, 0, + 1, 1, 1, 0, 0, 0, 0, + 1, 1, 1, 1, 0, 0, 0, 0, 0 + }; + PseudoRandom random; + const int32_t numSections = sizeof(pattern); + const int32_t framesPerSection = pulseLength / numSections; + for (int section = 0; section < numSections; section++) { + if (pattern[section]) { + for (int i = 0; i < framesPerSection; i++) { + mPulse.write((float) (random.nextRandomDouble() * mPulseAmplitude)); + } + } else { + for (int i = 0; i < framesPerSection; i++) { + mPulse.write(0.0f); + } + } + } + // Write any remaining frames. + int32_t framesWritten = framesPerSection * numSections; + for (int i = framesWritten; i < pulseLength; i++) { + mPulse.write(0.0f); + } + } + + void measureLatency() override { + // Smooth out the noise so we see rectangular blocks. + // This improves immunity against phase cancellation and distortion. + static constexpr float decay = 0.99f; // just under 1.0, lower numbers decay faster + mAudioRecording.detectPeaks(decay); + mPulse.detectPeaks(decay); + measureLatencyFromPulse(mAudioRecording, + mPulse, + &mLatencyReport); + } + }; #endif // ANALYZER_LATENCY_ANALYZER_H diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/analyzer/ManchesterEncoder.h b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/analyzer/ManchesterEncoder.h index b3d12b31..af1c8430 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/analyzer/ManchesterEncoder.h +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/analyzer/ManchesterEncoder.h @@ -41,15 +41,17 @@ public: , mCursor(samplesPerPulse) { } + virtual ~ManchesterEncoder() = default; + /** * This will be called when the next byte is needed. - * @return + * @return next byte */ virtual uint8_t onNextByte() = 0; /** * Generate the next floating point sample. - * @return + * @return next float */ virtual float nextFloat() { advanceSample(); @@ -64,10 +66,9 @@ protected: /** * This will be called when a new bit is ready to be encoded. * It can be used to prepare the encoded samples. - * @param current */ - virtual void onNextBit(bool current) {}; - + virtual void onNextBit(bool /* current */) {}; + void advanceSample() { // Are we ready for a new bit? if (++mCursor >= mSamplesPerPulse) { diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/analyzer/PeakDetector.h b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/analyzer/PeakDetector.h index 9139e429..4b3b4e71 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/analyzer/PeakDetector.h +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/analyzer/PeakDetector.h @@ -19,6 +19,11 @@ #include +/** + * Measure a peak envelope by rising with the peaks, + * and decaying exponentially after each peak. + * The absolute value of the input signal is used. + */ class PeakDetector { public: @@ -27,20 +32,35 @@ public: } double process(double input) { - mLevel *= mDecay; + mLevel *= mDecay; // exponential decay input = fabs(input); + // never fall below the input signal if (input > mLevel) { mLevel = input; } return mLevel; } - double getLevel() { + double getLevel() const { return mLevel; } + double getDecay() const { + return mDecay; + } + + /** + * Multiply the level by this amount on every iteration. + * This provides an exponential decay curve. + * A value just under 1.0 is best, for example, 0.99; + * @param decay scale level for each input + */ + void setDecay(double decay) { + mDecay = decay; + } + private: - static constexpr float kDefaultDecay = 0.99f; + static constexpr double kDefaultDecay = 0.99f; double mLevel = 0.0; double mDecay = kDefaultDecay; diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/analyzer/PseudoRandom.h b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/analyzer/PseudoRandom.h index 4aedbe0c..1c4938cb 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/analyzer/PseudoRandom.h +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/analyzer/PseudoRandom.h @@ -22,8 +22,7 @@ class PseudoRandom { public: - PseudoRandom() {} - PseudoRandom(int64_t seed) + PseudoRandom(int64_t seed = 99887766) : mSeed(seed) {} @@ -36,7 +35,8 @@ public: return nextRandomInteger() * (0.5 / (((int32_t)1) << 30)); } - /** Calculate random 32 bit number using linear-congruential method. + /** Calculate random 32 bit number using linear-congruential method + * with known real-time performance. */ int32_t nextRandomInteger() { #if __has_builtin(__builtin_mul_overflow) && __has_builtin(__builtin_add_overflow) @@ -51,7 +51,7 @@ public: } private: - int64_t mSeed = 99887766; + int64_t mSeed; }; #endif //ANALYZER_PSEUDORANDOM_H diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/analyzer/RandomPulseGenerator.h b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/analyzer/RandomPulseGenerator.h index f0623ccc..030050b4 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/analyzer/RandomPulseGenerator.h +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/analyzer/RandomPulseGenerator.h @@ -29,12 +29,14 @@ public: : RoundedManchesterEncoder(samplesPerPulse) { } + virtual ~RandomPulseGenerator() = default; + /** * This will be called when the next byte is needed. * @return random byte */ uint8_t onNextByte() override { - return static_cast(rand() & 0x00FF); + return static_cast(rand()); } }; diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/analyzer/RoundedManchesterEncoder.h b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/analyzer/RoundedManchesterEncoder.h index b1ba949e..f2eba840 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/analyzer/RoundedManchesterEncoder.h +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/analyzer/RoundedManchesterEncoder.h @@ -35,30 +35,30 @@ public: mZeroAfterZero = std::make_unique(samplesPerPulse); mZeroAfterOne = std::make_unique(samplesPerPulse); - int i = 0; - for (int j = 0; j < rampSize; j++) { - float phase = (j + 1) * M_PI / rampSize; + int sampleIndex = 0; + for (int rampIndex = 0; rampIndex < rampSize; rampIndex++) { + float phase = (rampIndex + 1) * M_PI / rampSize; float sample = -cosf(phase); - mZeroAfterZero[i] = sample; - mZeroAfterOne[i] = 1.0f; - i++; + mZeroAfterZero[sampleIndex] = sample; + mZeroAfterOne[sampleIndex] = 1.0f; + sampleIndex++; } - for (int j = 0; j < rampSize; j++) { - mZeroAfterZero[i] = 1.0f; - mZeroAfterOne[i] = 1.0f; - i++; + for (int rampIndex = 0; rampIndex < rampSize; rampIndex++) { + mZeroAfterZero[sampleIndex] = 1.0f; + mZeroAfterOne[sampleIndex] = 1.0f; + sampleIndex++; } - for (int j = 0; j < rampSize; j++) { - float phase = (j + 1) * M_PI / rampSize; + for (int rampIndex = 0; rampIndex < rampSize; rampIndex++) { + float phase = (rampIndex + 1) * M_PI / rampSize; float sample = cosf(phase); - mZeroAfterZero[i] = sample; - mZeroAfterOne[i] = sample; - i++; + mZeroAfterZero[sampleIndex] = sample; + mZeroAfterOne[sampleIndex] = sample; + sampleIndex++; } - for (int j = 0; j < rampSize; j++) { - mZeroAfterZero[i] = -1.0f; - mZeroAfterOne[i] = -1.0f; - i++; + for (int rampIndex = 0; rampIndex < rampSize; rampIndex++) { + mZeroAfterZero[sampleIndex] = -1.0f; + mZeroAfterOne[sampleIndex] = -1.0f; + sampleIndex++; } } @@ -70,7 +70,6 @@ public: mPreviousBit = current; } - float nextFloat() override { advanceSample(); float output = mCurrentSamples[mCursor]; diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/flowunits/ExponentialShape.cpp b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/flowunits/ExponentialShape.cpp index 4614306a..4aac2810 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/flowunits/ExponentialShape.cpp +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/flowunits/ExponentialShape.cpp @@ -32,4 +32,4 @@ int32_t ExponentialShape::onProcess(int32_t numFrames) { } return numFrames; -} \ No newline at end of file +} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/flowunits/ExponentialShape.h b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/flowunits/ExponentialShape.h index 4c110bd0..4c28e519 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/flowunits/ExponentialShape.h +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/flowunits/ExponentialShape.h @@ -25,7 +25,7 @@ * * The waveform is not band-limited so it will have aliasing artifacts at higher frequencies. */ -class ExponentialShape : public flowgraph::FlowGraphFilter { +class ExponentialShape : public oboe::flowgraph::FlowGraphFilter { public: ExponentialShape(); diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/flowunits/LinearShape.cpp b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/flowunits/LinearShape.cpp index ee2ade10..ebff5e24 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/flowunits/LinearShape.cpp +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/flowunits/LinearShape.cpp @@ -17,7 +17,7 @@ #include "LinearShape.h" -using namespace flowgraph; +using namespace oboe::flowgraph; LinearShape::LinearShape() : FlowGraphFilter(1) { @@ -33,4 +33,4 @@ int32_t LinearShape::onProcess(int numFrames) { } return numFrames; -} \ No newline at end of file +} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/flowunits/LinearShape.h b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/flowunits/LinearShape.h index 6cb2fec4..238870f5 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/flowunits/LinearShape.h +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/flowunits/LinearShape.h @@ -23,7 +23,7 @@ /** * Convert an input between -1.0 and +1.0 to a linear region between min and max. */ -class LinearShape : public flowgraph::FlowGraphFilter { +class LinearShape : public oboe::flowgraph::FlowGraphFilter { public: LinearShape(); diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/flowunits/OscillatorBase.cpp b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/flowunits/OscillatorBase.cpp index 361f4dbe..380447a9 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/flowunits/OscillatorBase.cpp +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/flowunits/OscillatorBase.cpp @@ -16,7 +16,7 @@ #include "OscillatorBase.h" -using namespace flowgraph; +using namespace oboe::flowgraph; OscillatorBase::OscillatorBase() : frequency(*this, 1) diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/flowunits/OscillatorBase.h b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/flowunits/OscillatorBase.h index 45ce7071..d3f2f459 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/flowunits/OscillatorBase.h +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/flowunits/OscillatorBase.h @@ -29,7 +29,7 @@ * This module has "frequency" and "amplitude" ports for control. */ -class OscillatorBase : public flowgraph::FlowGraphNode { +class OscillatorBase : public oboe::flowgraph::FlowGraphNode { public: OscillatorBase(); @@ -61,16 +61,16 @@ public: /** * Control the frequency of the oscillator in Hz. */ - flowgraph::FlowGraphPortFloatInput frequency; + oboe::flowgraph::FlowGraphPortFloatInput frequency; /** * Control the linear amplitude of the oscillator. * Silence is 0.0. * A typical full amplitude would be 1.0. */ - flowgraph::FlowGraphPortFloatInput amplitude; + oboe::flowgraph::FlowGraphPortFloatInput amplitude; - flowgraph::FlowGraphPortFloatOutput output; + oboe::flowgraph::FlowGraphPortFloatOutput output; protected: /** diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/flowunits/WhiteNoise.cpp b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/flowunits/WhiteNoise.cpp new file mode 100644 index 00000000..ecad91c6 --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/flowunits/WhiteNoise.cpp @@ -0,0 +1,32 @@ +/* + * 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 +#include + +#include "WhiteNoise.h" + +int32_t WhiteNoise::onProcess(int32_t numFrames) { + const float *amplitudes = amplitude.getBuffer(); + float *buffer = output.getBuffer(); + + for (int i = 0; i < numFrames; i++) { + float noise = (float) mPseudoRandom.nextRandomDouble(); // -1 to +1 + *buffer++ = noise * (*amplitudes++); + } + + return numFrames; +} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/flowunits/WhiteNoise.h b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/flowunits/WhiteNoise.h new file mode 100644 index 00000000..262ef81a --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/flowunits/WhiteNoise.h @@ -0,0 +1,57 @@ +/* + * 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 FLOWGRAPH_WHITE_NOISE_H +#define FLOWGRAPH_WHITE_NOISE_H + +#include + +#include "flowgraph/FlowGraphNode.h" + +#include "../analyzer/PseudoRandom.h" + +/** + * White noise with equal energy in all frequencies up to the Nyquist. + * This is a based on random numbers with a uniform distribution. + */ +class WhiteNoise : public oboe::flowgraph::FlowGraphNode { +public: + + WhiteNoise() + : oboe::flowgraph::FlowGraphNode() + , amplitude(*this, 1) + , output(*this, 1) + { + } + + virtual ~WhiteNoise() = default; + + int32_t onProcess(int32_t numFrames) override; + + /** + * Control the amplitude amplitude of the noise. + * Silence is 0.0. + * A typical full amplitude would be 1.0. + */ + oboe::flowgraph::FlowGraphPortFloatInput amplitude; + + oboe::flowgraph::FlowGraphPortFloatOutput output; + +private: + PseudoRandom mPseudoRandom; +}; + +#endif //FLOWGRAPH_WHITE_NOISE_H diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/jni-bridge.cpp b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/jni-bridge.cpp index 44d76d29..fad629ef 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/jni-bridge.cpp +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/jni-bridge.cpp @@ -20,14 +20,20 @@ #include #include #include +#include #include +#include "common/AdpfWrapper.h" #include "common/OboeDebug.h" #include "oboe/Oboe.h" #include "NativeAudioContext.h" +#include "TestColdStartLatency.h" +#include "TestErrorCallback.h" +#include "TestRoutingCrash.h" +#include "TestRapidCycle.h" -NativeAudioContext engine; +static NativeAudioContext engine; /*********************************************************************************/ /********************** JNI Prototypes *****************************************/ @@ -35,104 +41,138 @@ NativeAudioContext engine; extern "C" { JNIEXPORT jint JNICALL -Java_com_google_sample_oboe_manualtest_OboeAudioStream_openNative(JNIEnv *env, jobject, +Java_com_mobileer_oboetester_OboeAudioStream_openNative(JNIEnv *env, jobject, 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); JNIEXPORT void JNICALL -Java_com_google_sample_oboe_manualtest_OboeAudioStream_close(JNIEnv *env, jobject, jint); +Java_com_mobileer_oboetester_OboeAudioStream_close(JNIEnv *env, jobject, jint); JNIEXPORT jint JNICALL -Java_com_google_sample_oboe_manualtest_OboeAudioStream_setThresholdInFrames(JNIEnv *env, jobject, jint, jint); +Java_com_mobileer_oboetester_OboeAudioStream_setThresholdInFrames(JNIEnv *env, jobject, jint, jint); JNIEXPORT jint JNICALL -Java_com_google_sample_oboe_manualtest_OboeAudioStream_getThresholdInFrames(JNIEnv *env, jobject, jint); +Java_com_mobileer_oboetester_OboeAudioStream_getThresholdInFrames(JNIEnv *env, jobject, jint); JNIEXPORT jint JNICALL -Java_com_google_sample_oboe_manualtest_OboeAudioStream_getBufferCapacityInFrames(JNIEnv *env, jobject, jint); +Java_com_mobileer_oboetester_OboeAudioStream_getBufferCapacityInFrames(JNIEnv *env, jobject, jint); JNIEXPORT jint JNICALL -Java_com_google_sample_oboe_manualtest_OboeAudioStream_setNativeApi(JNIEnv *env, jobject, jint, jint); +Java_com_mobileer_oboetester_OboeAudioStream_setNativeApi(JNIEnv *env, jobject, jint, jint); JNIEXPORT void JNICALL -Java_com_google_sample_oboe_manualtest_OboeAudioStream_setUseCallback(JNIEnv *env, jclass type, +Java_com_mobileer_oboetester_OboeAudioStream_setUseCallback(JNIEnv *env, jclass type, jboolean useCallback); JNIEXPORT void JNICALL -Java_com_google_sample_oboe_manualtest_OboeAudioStream_setCallbackReturnStop(JNIEnv *env, +Java_com_mobileer_oboetester_OboeAudioStream_setCallbackReturnStop(JNIEnv *env, jclass type, jboolean b); JNIEXPORT void JNICALL -Java_com_google_sample_oboe_manualtest_OboeAudioStream_setCallbackSize(JNIEnv *env, jclass type, +Java_com_mobileer_oboetester_OboeAudioStream_setCallbackSize(JNIEnv *env, jclass type, jint callbackSize); // ================= OboeAudioOutputStream ================================ JNIEXPORT void JNICALL -Java_com_google_sample_oboe_manualtest_OboeAudioOutputStream_setToneEnabled(JNIEnv *env, jobject, jboolean); +Java_com_mobileer_oboetester_OboeAudioOutputStream_trigger(JNIEnv *env, jobject); JNIEXPORT void JNICALL -Java_com_google_sample_oboe_manualtest_OboeAudioOutputStream_setToneType(JNIEnv *env, jobject, jint); +Java_com_mobileer_oboetester_OboeAudioOutputStream_setToneType(JNIEnv *env, jobject, jint); JNIEXPORT void JNICALL -Java_com_google_sample_oboe_manualtest_OboeAudioOutputStream_setAmplitude(JNIEnv *env, jobject, jdouble); +Java_com_mobileer_oboetester_OboeAudioOutputStream_setAmplitude(JNIEnv *env, jobject, jfloat); /*********************************************************************************/ /********************** JNI Implementations *************************************/ /*********************************************************************************/ JNIEXPORT jboolean JNICALL -Java_com_google_sample_oboe_manualtest_NativeEngine_isMMapSupported(JNIEnv *env, jclass type) { - return AAudioExtensions::getInstance().isMMapSupported(); +Java_com_mobileer_oboetester_NativeEngine_isMMapSupported(JNIEnv *env, jclass type) { + return oboe::AAudioExtensions::getInstance().isMMapSupported(); } JNIEXPORT jboolean JNICALL -Java_com_google_sample_oboe_manualtest_NativeEngine_isMMapExclusiveSupported(JNIEnv *env, jclass type) { - return AAudioExtensions::getInstance().isMMapExclusiveSupported(); +Java_com_mobileer_oboetester_NativeEngine_isMMapExclusiveSupported(JNIEnv *env, jclass type) { + return oboe::AAudioExtensions::getInstance().isMMapExclusiveSupported(); } JNIEXPORT void JNICALL -Java_com_google_sample_oboe_manualtest_NativeEngine_setWorkaroundsEnabled(JNIEnv *env, jclass type, +Java_com_mobileer_oboetester_NativeEngine_setWorkaroundsEnabled(JNIEnv *env, jclass type, jboolean enabled) { oboe::OboeGlobals::setWorkaroundsEnabled(enabled); } +JNIEXPORT jboolean JNICALL +Java_com_mobileer_oboetester_NativeEngine_areWorkaroundsEnabled(JNIEnv *env, + jclass type) { + return oboe::OboeGlobals::areWorkaroundsEnabled(); +} + JNIEXPORT jint JNICALL -Java_com_google_sample_oboe_manualtest_OboeAudioStream_openNative( +Java_com_mobileer_oboetester_NativeEngine_getCpuCount(JNIEnv *env, jclass type) { + return sysconf(_SC_NPROCESSORS_CONF); +} + +JNIEXPORT void JNICALL +Java_com_mobileer_oboetester_NativeEngine_setCpuAffinityMask(JNIEnv *env, + jclass type, + jint mask) { + engine.getCurrentActivity()->setCpuAffinityMask(mask); +} + +JNIEXPORT void JNICALL +Java_com_mobileer_oboetester_NativeEngine_setWorkloadReportingEnabled(JNIEnv *env, + jclass type, + jboolean enabled) { + engine.getCurrentActivity()->setWorkloadReportingEnabled(enabled); +} + +JNIEXPORT jint JNICALL +Java_com_mobileer_oboetester_OboeAudioStream_openNative( JNIEnv *env, jobject synth, 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) { - LOGD("OboeAudioStream_openNative: sampleRate = %d, framesPerBurst = %d", sampleRate, framesPerBurst); + LOGD("OboeAudioStream_openNative: sampleRate = %d", sampleRate); return (jint) engine.getCurrentActivity()->open(nativeApi, sampleRate, channelCount, + channelMask, format, sharingMode, performanceMode, inputPreset, + usage, + contentType, + bufferCapacityInFrames, deviceId, sessionId, - framesPerBurst, channelConversionAllowed, formatConversionAllowed, rateConversionQuality, @@ -141,39 +181,54 @@ Java_com_google_sample_oboe_manualtest_OboeAudioStream_openNative( } JNIEXPORT jint JNICALL -Java_com_google_sample_oboe_manualtest_TestAudioActivity_startNative(JNIEnv *env, jobject) { +Java_com_mobileer_oboetester_TestAudioActivity_startNative(JNIEnv *env, jobject) { return (jint) engine.getCurrentActivity()->start(); } JNIEXPORT jint JNICALL -Java_com_google_sample_oboe_manualtest_TestAudioActivity_pauseNative(JNIEnv *env, jobject) { +Java_com_mobileer_oboetester_TestAudioActivity_pauseNative(JNIEnv *env, jobject) { return (jint) engine.getCurrentActivity()->pause(); } JNIEXPORT jint JNICALL -Java_com_google_sample_oboe_manualtest_TestAudioActivity_stopNative(JNIEnv *env, jobject) { +Java_com_mobileer_oboetester_TestAudioActivity_flushNative(JNIEnv *env, jobject) { + return (jint) engine.getCurrentActivity()->flush(); +} + +JNIEXPORT jint JNICALL +Java_com_mobileer_oboetester_TestAudioActivity_stopNative(JNIEnv *env, jobject) { return (jint) engine.getCurrentActivity()->stop(); } JNIEXPORT jint JNICALL -Java_com_google_sample_oboe_manualtest_TestAudioActivity_getFramesPerCallback(JNIEnv *env, jobject) { +Java_com_mobileer_oboetester_TestAudioActivity_releaseNative(JNIEnv *env, jobject) { + return (jint) engine.getCurrentActivity()->release(); +} + +JNIEXPORT jint JNICALL +Java_com_mobileer_oboetester_TestAudioActivity_getFramesPerCallback(JNIEnv *env, jobject) { return (jint) engine.getCurrentActivity()->getFramesPerCallback(); } JNIEXPORT jint JNICALL -Java_com_google_sample_oboe_manualtest_OboeAudioStream_startPlaybackNative(JNIEnv *env, jobject) { +Java_com_mobileer_oboetester_OboeAudioStream_startPlaybackNative(JNIEnv *env, jobject) { return (jint) engine.getCurrentActivity()->startPlayback(); } JNIEXPORT void JNICALL -Java_com_google_sample_oboe_manualtest_OboeAudioStream_close(JNIEnv *env, jobject, jint streamIndex) { +Java_com_mobileer_oboetester_OboeAudioStream_close(JNIEnv *env, jobject, jint streamIndex) { engine.getCurrentActivity()->close(streamIndex); } +JNIEXPORT void JNICALL +Java_com_mobileer_oboetester_TestAudioActivity_setUseAlternativeAdpf(JNIEnv *env, jobject, jboolean enabled) { + AdpfWrapper::setUseAlternative(enabled); +} + JNIEXPORT jint JNICALL -Java_com_google_sample_oboe_manualtest_OboeAudioStream_setBufferSizeInFrames( +Java_com_mobileer_oboetester_OboeAudioStream_setBufferSizeInFrames( JNIEnv *env, jobject, jint streamIndex, jint threshold) { - oboe::AudioStream *oboeStream = engine.getCurrentActivity()->getStream(streamIndex); + std::shared_ptr oboeStream = engine.getCurrentActivity()->getStream(streamIndex); if (oboeStream != nullptr) { auto result = oboeStream->setBufferSizeInFrames(threshold); return (!result) @@ -184,21 +239,30 @@ Java_com_google_sample_oboe_manualtest_OboeAudioStream_setBufferSizeInFrames( } JNIEXPORT jint JNICALL -Java_com_google_sample_oboe_manualtest_OboeAudioStream_getBufferSizeInFrames( +Java_com_mobileer_oboetester_OboeAudioStream_getBufferSizeInFrames( JNIEnv *env, jobject, jint streamIndex) { jint result = (jint) oboe::Result::ErrorNull; - oboe::AudioStream *oboeStream = engine.getCurrentActivity()->getStream(streamIndex); + std::shared_ptr oboeStream = engine.getCurrentActivity()->getStream(streamIndex); if (oboeStream != nullptr) { result = oboeStream->getBufferSizeInFrames(); } return result; } +JNIEXPORT void JNICALL +Java_com_mobileer_oboetester_OboeAudioStream_setPerformanceHintEnabled( + JNIEnv *env, jobject, jint streamIndex, jboolean enabled) { + std::shared_ptr oboeStream = engine.getCurrentActivity()->getStream(streamIndex); + if (oboeStream != nullptr) { + oboeStream->setPerformanceHintEnabled(enabled); + } +} + JNIEXPORT jint JNICALL -Java_com_google_sample_oboe_manualtest_OboeAudioStream_getBufferCapacityInFrames( +Java_com_mobileer_oboetester_OboeAudioStream_getBufferCapacityInFrames( JNIEnv *env, jobject, jint streamIndex) { jint result = (jint) oboe::Result::ErrorNull; - oboe::AudioStream *oboeStream = engine.getCurrentActivity()->getStream(streamIndex); + std::shared_ptr oboeStream = engine.getCurrentActivity()->getStream(streamIndex); if (oboeStream != nullptr) { result = oboeStream->getBufferCapacityInFrames(); } @@ -219,10 +283,10 @@ static int convertAudioApiToNativeApi(oboe::AudioApi audioApi) { } JNIEXPORT jint JNICALL -Java_com_google_sample_oboe_manualtest_OboeAudioStream_getNativeApi( +Java_com_mobileer_oboetester_OboeAudioStream_getNativeApi( JNIEnv *env, jobject, jint streamIndex) { jint result = (jint) oboe::Result::ErrorNull; - oboe::AudioStream *oboeStream = engine.getCurrentActivity()->getStream(streamIndex); + std::shared_ptr oboeStream = engine.getCurrentActivity()->getStream(streamIndex); if (oboeStream != nullptr) { oboe::AudioApi audioApi = oboeStream->getAudioApi(); result = convertAudioApiToNativeApi(audioApi); @@ -232,10 +296,10 @@ Java_com_google_sample_oboe_manualtest_OboeAudioStream_getNativeApi( } JNIEXPORT jint JNICALL -Java_com_google_sample_oboe_manualtest_OboeAudioStream_getSampleRate( +Java_com_mobileer_oboetester_OboeAudioStream_getSampleRate( JNIEnv *env, jobject, jint streamIndex) { jint result = (jint) oboe::Result::ErrorNull; - oboe::AudioStream *oboeStream = engine.getCurrentActivity()->getStream(streamIndex); + std::shared_ptr oboeStream = engine.getCurrentActivity()->getStream(streamIndex); if (oboeStream != nullptr) { result = oboeStream->getSampleRate(); } @@ -243,10 +307,10 @@ Java_com_google_sample_oboe_manualtest_OboeAudioStream_getSampleRate( } JNIEXPORT jint JNICALL -Java_com_google_sample_oboe_manualtest_OboeAudioStream_getSharingMode( +Java_com_mobileer_oboetester_OboeAudioStream_getSharingMode( JNIEnv *env, jobject, jint streamIndex) { jint result = (jint) oboe::Result::ErrorNull; - oboe::AudioStream *oboeStream = engine.getCurrentActivity()->getStream(streamIndex); + std::shared_ptr oboeStream = engine.getCurrentActivity()->getStream(streamIndex); if (oboeStream != nullptr) { result = (jint) oboeStream->getSharingMode(); } @@ -254,10 +318,10 @@ Java_com_google_sample_oboe_manualtest_OboeAudioStream_getSharingMode( } JNIEXPORT jint JNICALL -Java_com_google_sample_oboe_manualtest_OboeAudioStream_getPerformanceMode( +Java_com_mobileer_oboetester_OboeAudioStream_getPerformanceMode( JNIEnv *env, jobject, jint streamIndex) { jint result = (jint) oboe::Result::ErrorNull; - oboe::AudioStream *oboeStream = engine.getCurrentActivity()->getStream(streamIndex); + std::shared_ptr oboeStream = engine.getCurrentActivity()->getStream(streamIndex); if (oboeStream != nullptr) { result = (jint) oboeStream->getPerformanceMode(); } @@ -265,10 +329,10 @@ Java_com_google_sample_oboe_manualtest_OboeAudioStream_getPerformanceMode( } JNIEXPORT jint JNICALL -Java_com_google_sample_oboe_manualtest_OboeAudioStream_getInputPreset( +Java_com_mobileer_oboetester_OboeAudioStream_getInputPreset( JNIEnv *env, jobject, jint streamIndex) { jint result = (jint) oboe::Result::ErrorNull; - oboe::AudioStream *oboeStream = engine.getCurrentActivity()->getStream(streamIndex); + std::shared_ptr oboeStream = engine.getCurrentActivity()->getStream(streamIndex); if (oboeStream != nullptr) { result = (jint) oboeStream->getInputPreset(); } @@ -276,10 +340,10 @@ Java_com_google_sample_oboe_manualtest_OboeAudioStream_getInputPreset( } JNIEXPORT jint JNICALL -Java_com_google_sample_oboe_manualtest_OboeAudioStream_getFramesPerBurst( +Java_com_mobileer_oboetester_OboeAudioStream_getFramesPerBurst( JNIEnv *env, jobject, jint streamIndex) { jint result = (jint) oboe::Result::ErrorNull; - oboe::AudioStream *oboeStream = engine.getCurrentActivity()->getStream(streamIndex); + std::shared_ptr oboeStream = engine.getCurrentActivity()->getStream(streamIndex); if (oboeStream != nullptr) { result = oboeStream->getFramesPerBurst(); } @@ -287,10 +351,10 @@ Java_com_google_sample_oboe_manualtest_OboeAudioStream_getFramesPerBurst( } JNIEXPORT jint JNICALL -Java_com_google_sample_oboe_manualtest_OboeAudioStream_getChannelCount( +Java_com_mobileer_oboetester_OboeAudioStream_getChannelCount( JNIEnv *env, jobject, jint streamIndex) { jint result = (jint) oboe::Result::ErrorNull; - oboe::AudioStream *oboeStream = engine.getCurrentActivity()->getStream(streamIndex); + std::shared_ptr oboeStream = engine.getCurrentActivity()->getStream(streamIndex); if (oboeStream != nullptr) { result = oboeStream->getChannelCount(); } @@ -298,9 +362,20 @@ Java_com_google_sample_oboe_manualtest_OboeAudioStream_getChannelCount( } JNIEXPORT jint JNICALL -Java_com_google_sample_oboe_manualtest_OboeAudioStream_getFormat(JNIEnv *env, jobject instance, jint streamIndex) { - jint result = (jint) oboe::Result::ErrorNull; - oboe::AudioStream *oboeStream = engine.getCurrentActivity()->getStream(streamIndex); +Java_com_mobileer_oboetester_OboeAudioStream_getChannelMask( + JNIEnv *env, jobject, jint streamIndex) { + jint result = (jint) oboe::Result::ErrorNull; + std::shared_ptr oboeStream = engine.getCurrentActivity()->getStream(streamIndex); + if (oboeStream != nullptr) { + result = (jint) oboeStream->getChannelMask(); + } + return result; +} + +JNIEXPORT jint JNICALL +Java_com_mobileer_oboetester_OboeAudioStream_getFormat(JNIEnv *env, jobject instance, jint streamIndex) { + jint result = (jint) oboe::Result::ErrorNull; + std::shared_ptr oboeStream = engine.getCurrentActivity()->getStream(streamIndex); if (oboeStream != nullptr) { result = (jint) oboeStream->getFormat(); } @@ -308,10 +383,62 @@ Java_com_google_sample_oboe_manualtest_OboeAudioStream_getFormat(JNIEnv *env, jo } JNIEXPORT jint JNICALL -Java_com_google_sample_oboe_manualtest_OboeAudioStream_getDeviceId( +Java_com_mobileer_oboetester_OboeAudioStream_getHardwareChannelCount( JNIEnv *env, jobject, jint streamIndex) { jint result = (jint) oboe::Result::ErrorNull; - oboe::AudioStream *oboeStream = engine.getCurrentActivity()->getStream(streamIndex); + std::shared_ptr oboeStream = engine.getCurrentActivity()->getStream(streamIndex); + if (oboeStream != nullptr) { + result = oboeStream->getHardwareChannelCount(); + } + return result; +} + +JNIEXPORT jint JNICALL +Java_com_mobileer_oboetester_OboeAudioStream_getHardwareFormat(JNIEnv *env, jobject instance, jint streamIndex) { + jint result = (jint) oboe::Result::ErrorNull; + std::shared_ptr oboeStream = engine.getCurrentActivity()->getStream(streamIndex); + if (oboeStream != nullptr) { + result = (jint) oboeStream->getHardwareFormat(); + } + return result; +} + +JNIEXPORT jint JNICALL +Java_com_mobileer_oboetester_OboeAudioStream_getHardwareSampleRate( + JNIEnv *env, jobject, jint streamIndex) { + jint result = (jint) oboe::Result::ErrorNull; + std::shared_ptr oboeStream = engine.getCurrentActivity()->getStream(streamIndex); + if (oboeStream != nullptr) { + result = oboeStream->getHardwareSampleRate(); + } + return result; +} + +JNIEXPORT jint JNICALL +Java_com_mobileer_oboetester_OboeAudioStream_getUsage(JNIEnv *env, jobject instance, jint streamIndex) { + jint result = (jint) oboe::Result::ErrorNull; + std::shared_ptr oboeStream = engine.getCurrentActivity()->getStream(streamIndex); + if (oboeStream != nullptr) { + result = (jint) oboeStream->getUsage(); + } + return result; +} + +JNIEXPORT jint JNICALL +Java_com_mobileer_oboetester_OboeAudioStream_getContentType(JNIEnv *env, jobject instance, jint streamIndex) { + jint result = (jint) oboe::Result::ErrorNull; + std::shared_ptr oboeStream = engine.getCurrentActivity()->getStream(streamIndex); + if (oboeStream != nullptr) { + result = (jint) oboeStream->getContentType(); + } + return result; +} + +JNIEXPORT jint JNICALL +Java_com_mobileer_oboetester_OboeAudioStream_getDeviceId( + JNIEnv *env, jobject, jint streamIndex) { + jint result = (jint) oboe::Result::ErrorNull; + std::shared_ptr oboeStream = engine.getCurrentActivity()->getStream(streamIndex); if (oboeStream != nullptr) { result = oboeStream->getDeviceId(); } @@ -319,10 +446,10 @@ Java_com_google_sample_oboe_manualtest_OboeAudioStream_getDeviceId( } JNIEXPORT jint JNICALL -Java_com_google_sample_oboe_manualtest_OboeAudioStream_getSessionId( +Java_com_mobileer_oboetester_OboeAudioStream_getSessionId( JNIEnv *env, jobject, jint streamIndex) { jint result = (jint) oboe::Result::ErrorNull; - oboe::AudioStream *oboeStream = engine.getCurrentActivity()->getStream(streamIndex); + std::shared_ptr oboeStream = engine.getCurrentActivity()->getStream(streamIndex); if (oboeStream != nullptr) { result = oboeStream->getSessionId(); } @@ -330,10 +457,10 @@ Java_com_google_sample_oboe_manualtest_OboeAudioStream_getSessionId( } JNIEXPORT jlong JNICALL -Java_com_google_sample_oboe_manualtest_OboeAudioStream_getFramesWritten( +Java_com_mobileer_oboetester_OboeAudioStream_getFramesWritten( JNIEnv *env, jobject, jint streamIndex) { jlong result = (jint) oboe::Result::ErrorNull; - oboe::AudioStream *oboeStream = engine.getCurrentActivity()->getStream(streamIndex); + std::shared_ptr oboeStream = engine.getCurrentActivity()->getStream(streamIndex); if (oboeStream != nullptr) { result = oboeStream->getFramesWritten(); } @@ -341,10 +468,10 @@ Java_com_google_sample_oboe_manualtest_OboeAudioStream_getFramesWritten( } JNIEXPORT jlong JNICALL -Java_com_google_sample_oboe_manualtest_OboeAudioStream_getFramesRead( +Java_com_mobileer_oboetester_OboeAudioStream_getFramesRead( JNIEnv *env, jobject, jint streamIndex) { jlong result = (jlong) oboe::Result::ErrorNull; - oboe::AudioStream *oboeStream = engine.getCurrentActivity()->getStream(streamIndex); + std::shared_ptr oboeStream = engine.getCurrentActivity()->getStream(streamIndex); if (oboeStream != nullptr) { result = oboeStream->getFramesRead(); } @@ -352,10 +479,10 @@ Java_com_google_sample_oboe_manualtest_OboeAudioStream_getFramesRead( } JNIEXPORT jint JNICALL -Java_com_google_sample_oboe_manualtest_OboeAudioStream_getXRunCount( +Java_com_mobileer_oboetester_OboeAudioStream_getXRunCount( JNIEnv *env, jobject, jint streamIndex) { jint result = (jlong) oboe::Result::ErrorNull; - oboe::AudioStream *oboeStream = engine.getCurrentActivity()->getStream(streamIndex); + std::shared_ptr oboeStream = engine.getCurrentActivity()->getStream(streamIndex); if (oboeStream != nullptr) { auto oboeResult = oboeStream->getXRunCount(); if (!oboeResult) { @@ -368,38 +495,67 @@ Java_com_google_sample_oboe_manualtest_OboeAudioStream_getXRunCount( } JNIEXPORT jlong JNICALL -Java_com_google_sample_oboe_manualtest_OboeAudioStream_getCallbackCount( +Java_com_mobileer_oboetester_OboeAudioStream_getCallbackCount( JNIEnv *env, jobject) { return engine.getCurrentActivity()->getCallbackCount(); } -JNIEXPORT jdouble JNICALL -Java_com_google_sample_oboe_manualtest_OboeAudioStream_getLatency(JNIEnv *env, jobject instance, jint streamIndex) { - oboe::AudioStream *oboeStream = engine.getCurrentActivity()->getStream(streamIndex); +JNIEXPORT jint JNICALL +Java_com_mobileer_oboetester_OboeAudioStream_getLastErrorCallbackResult( + JNIEnv *env, jobject, jint streamIndex) { + std::shared_ptr oboeStream = engine.getCurrentActivity()->getStream(streamIndex); if (oboeStream != nullptr) { - auto result = oboeStream->calculateLatencyMillis(); - return (!result) ? -1.0 : result.value(); + return (jint) oboeStream->getLastErrorCallbackResult(); } - return -1.0; + return 0; } JNIEXPORT jdouble JNICALL -Java_com_google_sample_oboe_manualtest_OboeAudioStream_getCpuLoad(JNIEnv *env, jobject instance, jint streamIndex) { +Java_com_mobileer_oboetester_OboeAudioStream_getTimestampLatency(JNIEnv *env, + jobject instance, + jint streamIndex) { + return engine.getCurrentActivity()->getTimestampLatency(streamIndex); +} + +JNIEXPORT jfloat JNICALL +Java_com_mobileer_oboetester_OboeAudioStream_getCpuLoad(JNIEnv *env, jobject instance, jint streamIndex) { return engine.getCurrentActivity()->getCpuLoad(); } -JNIEXPORT void JNICALL -Java_com_google_sample_oboe_manualtest_OboeAudioStream_setWorkload( - JNIEnv *env, jobject, jdouble workload) { - engine.getCurrentActivity()->setWorkload(workload); +JNIEXPORT jfloat JNICALL +Java_com_mobileer_oboetester_OboeAudioStream_getAndResetMaxCpuLoad(JNIEnv *env, jobject instance, jint streamIndex) { + return engine.getCurrentActivity()->getAndResetMaxCpuLoad(); } JNIEXPORT jint JNICALL -Java_com_google_sample_oboe_manualtest_OboeAudioStream_getState(JNIEnv *env, jobject instance, jint streamIndex) { - oboe::AudioStream *oboeStream = engine.getCurrentActivity()->getStream(streamIndex); +Java_com_mobileer_oboetester_OboeAudioStream_getAndResetCpuMask(JNIEnv *env, jobject instance, jint streamIndex) { + return (jint) engine.getCurrentActivity()->getAndResetCpuMask(); +} + +JNIEXPORT jstring JNICALL +Java_com_mobileer_oboetester_OboeAudioStream_getCallbackTimeString(JNIEnv *env, jobject instance) { + return env->NewStringUTF(engine.getCurrentActivity()->getCallbackTimeString().c_str()); +} + +JNIEXPORT void JNICALL +Java_com_mobileer_oboetester_OboeAudioStream_setWorkload( + JNIEnv *env, jobject, jint workload) { + engine.getCurrentActivity()->setWorkload(workload); +} + +JNIEXPORT void JNICALL +Java_com_mobileer_oboetester_OboeAudioStream_setHearWorkload( + JNIEnv *env, jobject, jint streamIndex, jboolean enabled) { + engine.getCurrentActivity()->setHearWorkload(enabled); +} + +JNIEXPORT jint JNICALL +Java_com_mobileer_oboetester_OboeAudioStream_getState(JNIEnv *env, jobject instance, jint streamIndex) { + std::shared_ptr oboeStream = engine.getCurrentActivity()->getStream(streamIndex); if (oboeStream != nullptr) { auto state = oboeStream->getState(); - if (state != oboe::StreamState::Starting && state != oboe::StreamState::Started) { + if (state != oboe::StreamState::Starting && state != oboe::StreamState::Started + && state != oboe::StreamState::Disconnected) { oboe::Result result = oboeStream->waitForStateChange( oboe::StreamState::Uninitialized, &state, 0); @@ -420,70 +576,75 @@ Java_com_google_sample_oboe_manualtest_OboeAudioStream_getState(JNIEnv *env, job } JNIEXPORT jdouble JNICALL -Java_com_google_sample_oboe_manualtest_AudioInputTester_getPeakLevel(JNIEnv *env, +Java_com_mobileer_oboetester_AudioInputTester_getPeakLevel(JNIEnv *env, jobject instance, jint index) { return engine.getCurrentActivity()->getPeakLevel(index); } JNIEXPORT void JNICALL -Java_com_google_sample_oboe_manualtest_OboeAudioStream_setUseCallback(JNIEnv *env, jclass type, +Java_com_mobileer_oboetester_OboeAudioStream_setUseCallback(JNIEnv *env, jclass type, jboolean useCallback) { ActivityContext::mUseCallback = useCallback; } JNIEXPORT void JNICALL -Java_com_google_sample_oboe_manualtest_OboeAudioStream_setCallbackReturnStop(JNIEnv *env, jclass type, +Java_com_mobileer_oboetester_OboeAudioStream_setCallbackReturnStop(JNIEnv *env, jclass type, jboolean b) { OboeStreamCallbackProxy::setCallbackReturnStop(b); } JNIEXPORT void JNICALL -Java_com_google_sample_oboe_manualtest_OboeAudioStream_setCallbackSize(JNIEnv *env, jclass type, +Java_com_mobileer_oboetester_OboeAudioStream_setHangTimeMillis(JNIEnv *env, jclass type, + jint hangTimeMillis) { + OboeTesterStreamCallback::setHangTimeMillis(hangTimeMillis); +} + +JNIEXPORT void JNICALL +Java_com_mobileer_oboetester_OboeAudioStream_setCallbackSize(JNIEnv *env, jclass type, jint callbackSize) { ActivityContext::callbackSize = callbackSize; } JNIEXPORT jboolean JNICALL -Java_com_google_sample_oboe_manualtest_OboeAudioStream_isMMap(JNIEnv *env, jobject instance, jint streamIndex) { +Java_com_mobileer_oboetester_OboeAudioStream_isMMap(JNIEnv *env, jobject instance, jint streamIndex) { return engine.getCurrentActivity()->isMMapUsed(streamIndex); } // ================= OboeAudioOutputStream ================================ JNIEXPORT void JNICALL -Java_com_google_sample_oboe_manualtest_OboeAudioOutputStream_setToneEnabled( - JNIEnv *env, jobject, jboolean enabled) { - engine.getCurrentActivity()->setEnabled(enabled); +Java_com_mobileer_oboetester_OboeAudioOutputStream_trigger( + JNIEnv *env, jobject) { + engine.getCurrentActivity()->trigger(); } JNIEXPORT void JNICALL -Java_com_google_sample_oboe_manualtest_OboeAudioOutputStream_setToneType( - JNIEnv *env, jobject, jint toneType) { -// FIXME engine.getCurrentActivity()->setToneType(toneType); -} - -JNIEXPORT void JNICALL -Java_com_google_sample_oboe_manualtest_OboeAudioOutputStream_setChannelEnabled( +Java_com_mobileer_oboetester_OboeAudioOutputStream_setChannelEnabled( JNIEnv *env, jobject, jint channelIndex, jboolean enabled) { engine.getCurrentActivity()->setChannelEnabled(channelIndex, enabled); } JNIEXPORT void JNICALL -Java_com_google_sample_oboe_manualtest_OboeAudioOutputStream_setSignalType( +Java_com_mobileer_oboetester_OboeAudioOutputStream_setSignalType( JNIEnv *env, jobject, jint signalType) { engine.getCurrentActivity()->setSignalType(signalType); } +JNIEXPORT void JNICALL +Java_com_mobileer_oboetester_OboeAudioOutputStream_setAmplitude(JNIEnv *env, jobject, jfloat amplitude) { + engine.getCurrentActivity()->setAmplitude(amplitude); +} + JNIEXPORT jint JNICALL -Java_com_google_sample_oboe_manualtest_OboeAudioStream_getOboeVersionNumber(JNIEnv *env, +Java_com_mobileer_oboetester_OboeAudioStream_getOboeVersionNumber(JNIEnv *env, jclass type) { return OBOE_VERSION_NUMBER; } // ========================================================================== JNIEXPORT void JNICALL -Java_com_google_sample_oboe_manualtest_TestAudioActivity_setActivityType(JNIEnv *env, +Java_com_mobileer_oboetester_TestAudioActivity_setActivityType(JNIEnv *env, jobject instance, jint activityType) { engine.setActivityType(activityType); @@ -491,7 +652,7 @@ Java_com_google_sample_oboe_manualtest_TestAudioActivity_setActivityType(JNIEnv // ========================================================================== JNIEXPORT jint JNICALL -Java_com_google_sample_oboe_manualtest_TestInputActivity_saveWaveFile(JNIEnv *env, +Java_com_mobileer_oboetester_TestInputActivity_saveWaveFile(JNIEnv *env, jobject instance, jstring fileName) { const char *str = env->GetStringUTFChars(fileName, nullptr); @@ -503,7 +664,7 @@ Java_com_google_sample_oboe_manualtest_TestInputActivity_saveWaveFile(JNIEnv *en // ========================================================================== JNIEXPORT void JNICALL -Java_com_google_sample_oboe_manualtest_TestInputActivity_setMinimumFramesBeforeRead(JNIEnv *env, +Java_com_mobileer_oboetester_TestInputActivity_setMinimumFramesBeforeRead(JNIEnv *env, jobject instance, jint numFrames) { engine.getCurrentActivity()->setMinimumFramesBeforeRead(numFrames); @@ -511,95 +672,162 @@ Java_com_google_sample_oboe_manualtest_TestInputActivity_setMinimumFramesBeforeR // ========================================================================== JNIEXPORT void JNICALL -Java_com_google_sample_oboe_manualtest_EchoActivity_setDelayTime(JNIEnv *env, +Java_com_mobileer_oboetester_EchoActivity_setDelayTime(JNIEnv *env, jobject instance, jdouble delayTimeSeconds) { engine.setDelayTime(delayTimeSeconds); } +JNIEXPORT int JNICALL +Java_com_mobileer_oboetester_EchoActivity_getColdStartInputMillis(JNIEnv *env, + jobject instance) { + return engine.getCurrentActivity()->getColdStartInputMillis(); +} + +JNIEXPORT int JNICALL +Java_com_mobileer_oboetester_EchoActivity_getColdStartOutputMillis(JNIEnv *env, + jobject instance) { + return engine.getCurrentActivity()->getColdStartOutputMillis(); +} + // ========================================================================== JNIEXPORT jint JNICALL -Java_com_google_sample_oboe_manualtest_RoundTripLatencyActivity_getAnalyzerProgress(JNIEnv *env, +Java_com_mobileer_oboetester_RoundTripLatencyActivity_getAnalyzerProgress(JNIEnv *env, jobject instance) { return engine.mActivityRoundTripLatency.getLatencyAnalyzer()->getProgress(); } JNIEXPORT jint JNICALL -Java_com_google_sample_oboe_manualtest_RoundTripLatencyActivity_getMeasuredLatency(JNIEnv *env, +Java_com_mobileer_oboetester_RoundTripLatencyActivity_getMeasuredLatency(JNIEnv *env, jobject instance) { return engine.mActivityRoundTripLatency.getLatencyAnalyzer()->getMeasuredLatency(); } JNIEXPORT jdouble JNICALL -Java_com_google_sample_oboe_manualtest_RoundTripLatencyActivity_getMeasuredConfidence(JNIEnv *env, - jobject instance) { +Java_com_mobileer_oboetester_RoundTripLatencyActivity_getMeasuredConfidence(JNIEnv *env, + jobject instance) { return engine.mActivityRoundTripLatency.getLatencyAnalyzer()->getMeasuredConfidence(); } JNIEXPORT jdouble JNICALL -Java_com_google_sample_oboe_manualtest_RoundTripLatencyActivity_getBackgroundRMS(JNIEnv *env, +Java_com_mobileer_oboetester_RoundTripLatencyActivity_getMeasuredCorrelation(JNIEnv *env, + jobject instance) { + return engine.mActivityRoundTripLatency.getLatencyAnalyzer()->getMeasuredCorrelation(); +} + +JNIEXPORT jdouble JNICALL +Java_com_mobileer_oboetester_RoundTripLatencyActivity_measureTimestampLatency(JNIEnv *env, + jobject instance) { + return engine.mActivityRoundTripLatency.measureTimestampLatency(); +} + +JNIEXPORT jdouble JNICALL +Java_com_mobileer_oboetester_RoundTripLatencyActivity_getBackgroundRMS(JNIEnv *env, jobject instance) { return engine.mActivityRoundTripLatency.getLatencyAnalyzer()->getBackgroundRMS(); } JNIEXPORT jdouble JNICALL -Java_com_google_sample_oboe_manualtest_RoundTripLatencyActivity_getSignalRMS(JNIEnv *env, +Java_com_mobileer_oboetester_RoundTripLatencyActivity_getSignalRMS(JNIEnv *env, jobject instance) { return engine.mActivityRoundTripLatency.getLatencyAnalyzer()->getSignalRMS(); } JNIEXPORT jint JNICALL -Java_com_google_sample_oboe_manualtest_AnalyzerActivity_getMeasuredResult(JNIEnv *env, +Java_com_mobileer_oboetester_AnalyzerActivity_getMeasuredResult(JNIEnv *env, jobject instance) { return engine.mActivityRoundTripLatency.getLatencyAnalyzer()->getResult(); } // ========================================================================== JNIEXPORT jint JNICALL -Java_com_google_sample_oboe_manualtest_AnalyzerActivity_getAnalyzerState(JNIEnv *env, +Java_com_mobileer_oboetester_AnalyzerActivity_getAnalyzerState(JNIEnv *env, jobject instance) { return ((ActivityFullDuplex *)engine.getCurrentActivity())->getState(); } JNIEXPORT jboolean JNICALL -Java_com_google_sample_oboe_manualtest_AnalyzerActivity_isAnalyzerDone(JNIEnv *env, +Java_com_mobileer_oboetester_AnalyzerActivity_isAnalyzerDone(JNIEnv *env, jobject instance) { return ((ActivityFullDuplex *)engine.getCurrentActivity())->isAnalyzerDone(); } JNIEXPORT jint JNICALL -Java_com_google_sample_oboe_manualtest_AnalyzerActivity_getResetCount(JNIEnv *env, +Java_com_mobileer_oboetester_AnalyzerActivity_getResetCount(JNIEnv *env, jobject instance) { return ((ActivityFullDuplex *)engine.getCurrentActivity())->getResetCount(); } // ========================================================================== JNIEXPORT jint JNICALL -Java_com_google_sample_oboe_manualtest_GlitchActivity_getGlitchCount(JNIEnv *env, - jobject instance) { +Java_com_mobileer_oboetester_GlitchActivity_getGlitchCount(JNIEnv *env, + jobject instance) { return engine.mActivityGlitches.getGlitchAnalyzer()->getGlitchCount(); } JNIEXPORT jint JNICALL -Java_com_google_sample_oboe_manualtest_GlitchActivity_getStateFrameCount(JNIEnv *env, +Java_com_mobileer_oboetester_GlitchActivity_getGlitchLength(JNIEnv *env, + jobject instance) { + return engine.mActivityGlitches.getGlitchAnalyzer()->getGlitchLength(); +} + +JNIEXPORT double JNICALL +Java_com_mobileer_oboetester_GlitchActivity_getPhase(JNIEnv *env, + jobject instance) { + return engine.mActivityGlitches.getGlitchAnalyzer()->getPhaseOffset(); +} + +JNIEXPORT jint JNICALL +Java_com_mobileer_oboetester_GlitchActivity_getStateFrameCount(JNIEnv *env, jobject instance, jint state) { return engine.mActivityGlitches.getGlitchAnalyzer()->getStateFrameCount(state); } JNIEXPORT jdouble JNICALL -Java_com_google_sample_oboe_manualtest_GlitchActivity_getSignalToNoiseDB(JNIEnv *env, +Java_com_mobileer_oboetester_GlitchActivity_getSignalToNoiseDB(JNIEnv *env, jobject instance) { return engine.mActivityGlitches.getGlitchAnalyzer()->getSignalToNoiseDB(); } + JNIEXPORT jdouble JNICALL -Java_com_google_sample_oboe_manualtest_GlitchActivity_getPeakAmplitude(JNIEnv *env, +Java_com_mobileer_oboetester_GlitchActivity_getPeakAmplitude(JNIEnv *env, jobject instance) { return engine.mActivityGlitches.getGlitchAnalyzer()->getPeakAmplitude(); } +JNIEXPORT jdouble JNICALL +Java_com_mobileer_oboetester_GlitchActivity_getSineAmplitude(JNIEnv *env, + jobject instance) { + return engine.mActivityGlitches.getGlitchAnalyzer()->getSineAmplitude(); +} + +JNIEXPORT jint JNICALL +Java_com_mobileer_oboetester_GlitchActivity_getSinePeriod(JNIEnv *env, + jobject instance) { + return engine.mActivityGlitches.getGlitchAnalyzer()->getSinePeriod(); +} + +JNIEXPORT jdouble JNICALL +Java_com_mobileer_oboetester_TestDataPathsActivity_getMagnitude(JNIEnv *env, + jobject instance) { + return engine.mActivityDataPath.getDataPathAnalyzer()->getMagnitude(); +} + +JNIEXPORT jdouble JNICALL +Java_com_mobileer_oboetester_TestDataPathsActivity_getMaxMagnitude(JNIEnv *env, + jobject instance) { + return engine.mActivityDataPath.getDataPathAnalyzer()->getMaxMagnitude(); +} + +JNIEXPORT double JNICALL +Java_com_mobileer_oboetester_TestDataPathsActivity_getPhaseDataPaths(JNIEnv *env, + jobject instance) { + return engine.mActivityDataPath.getDataPathAnalyzer()->getPhaseOffset(); +} + JNIEXPORT void JNICALL -Java_com_google_sample_oboe_manualtest_GlitchActivity_setTolerance(JNIEnv *env, +Java_com_mobileer_oboetester_GlitchActivity_setTolerance(JNIEnv *env, jobject instance, jfloat tolerance) { if (engine.mActivityGlitches.getGlitchAnalyzer()) { @@ -607,8 +835,41 @@ Java_com_google_sample_oboe_manualtest_GlitchActivity_setTolerance(JNIEnv *env, } } +JNIEXPORT void JNICALL +Java_com_mobileer_oboetester_GlitchActivity_setForcedGlitchDuration(JNIEnv *env, + jobject instance, + jint frames) { + if (engine.mActivityGlitches.getGlitchAnalyzer()) { + engine.mActivityGlitches.getGlitchAnalyzer()->setForcedGlitchDuration(frames); + } +} + +JNIEXPORT void JNICALL +Java_com_mobileer_oboetester_GlitchActivity_setInputChannelNative(JNIEnv *env, + jobject instance, + jint channel) { + if (engine.mActivityGlitches.getGlitchAnalyzer()) { + engine.mActivityGlitches.getGlitchAnalyzer()->setInputChannel(channel); + } + if (engine.mActivityDataPath.getDataPathAnalyzer()) { + engine.mActivityDataPath.getDataPathAnalyzer()->setInputChannel(channel); + } +} + +JNIEXPORT void JNICALL +Java_com_mobileer_oboetester_GlitchActivity_setOutputChannelNative(JNIEnv *env, + jobject instance, + jint channel) { + if (engine.mActivityGlitches.getGlitchAnalyzer()) { + engine.mActivityGlitches.getGlitchAnalyzer()->setOutputChannel(channel); + } + if (engine.mActivityDataPath.getDataPathAnalyzer()) { + engine.mActivityDataPath.getDataPathAnalyzer()->setOutputChannel(channel); + } +} + JNIEXPORT jint JNICALL -Java_com_google_sample_oboe_manualtest_ManualGlitchActivity_getGlitch(JNIEnv *env, jobject instance, +Java_com_mobileer_oboetester_ManualGlitchActivity_getGlitch(JNIEnv *env, jobject instance, jfloatArray waveform_) { float *waveform = env->GetFloatArrayElements(waveform_, nullptr); jsize length = env->GetArrayLength(waveform_); @@ -622,4 +883,125 @@ Java_com_google_sample_oboe_manualtest_ManualGlitchActivity_getGlitch(JNIEnv *en return numSamples; } +JNIEXPORT jint JNICALL +Java_com_mobileer_oboetester_ManualGlitchActivity_getRecentSamples(JNIEnv *env, jobject instance, + jfloatArray waveform_) { + float *waveform = env->GetFloatArrayElements(waveform_, nullptr); + jsize length = env->GetArrayLength(waveform_); + jsize numSamples = 0; + auto *analyzer = engine.mActivityGlitches.getGlitchAnalyzer(); + if (analyzer) { + numSamples = analyzer->getRecentSamples(waveform, length); + } + + env->ReleaseFloatArrayElements(waveform_, waveform, 0); + return numSamples; } + +JNIEXPORT void JNICALL +Java_com_mobileer_oboetester_TestAudioActivity_setDefaultAudioValues(JNIEnv *env, jclass clazz, + jint audio_manager_sample_rate, + jint audio_manager_frames_per_burst) { + oboe::DefaultStreamValues::SampleRate = audio_manager_sample_rate; + oboe::DefaultStreamValues::FramesPerBurst = audio_manager_frames_per_burst; +} + +static TestErrorCallback sErrorCallbackTester; + +JNIEXPORT void JNICALL +Java_com_mobileer_oboetester_TestErrorCallbackActivity_testDeleteCrash( + JNIEnv *env, jobject instance) { + sErrorCallbackTester.test(); +} + +JNIEXPORT jint JNICALL +Java_com_mobileer_oboetester_TestErrorCallbackActivity_getCallbackMagic( + JNIEnv *env, jobject instance) { + return sErrorCallbackTester.getCallbackMagic(); +} + +static TestRoutingCrash sRoutingCrash; + +JNIEXPORT jint JNICALL +Java_com_mobileer_oboetester_TestRouteDuringCallbackActivity_startStream( + JNIEnv *env, jobject instance, + jboolean useInput) { + return sRoutingCrash.start(useInput); +} + +JNIEXPORT jint JNICALL +Java_com_mobileer_oboetester_TestRouteDuringCallbackActivity_stopStream( + JNIEnv *env, jobject instance) { + return sRoutingCrash.stop(); +} + +JNIEXPORT jint JNICALL +Java_com_mobileer_oboetester_TestRouteDuringCallbackActivity_getSleepTimeMicros( + JNIEnv *env, jobject instance) { + return sRoutingCrash.getSleepTimeMicros(); +} + +static TestColdStartLatency sColdStartLatency; + +JNIEXPORT jint JNICALL +Java_com_mobileer_oboetester_TestColdStartLatencyActivity_openStream( + JNIEnv *env, jobject instance, + jboolean useInput, jboolean useLowLatency, jboolean useMmap, jboolean useExclusive) { + return sColdStartLatency.open(useInput, useLowLatency, useMmap, useExclusive); +} + +JNIEXPORT jint JNICALL +Java_com_mobileer_oboetester_TestColdStartLatencyActivity_startStream( + JNIEnv *env, jobject instance) { + return sColdStartLatency.start(); +} + +JNIEXPORT jint JNICALL +Java_com_mobileer_oboetester_TestColdStartLatencyActivity_closeStream( + JNIEnv *env, jobject instance) { + return sColdStartLatency.close(); +} + +JNIEXPORT jint JNICALL +Java_com_mobileer_oboetester_TestColdStartLatencyActivity_getOpenTimeMicros( + JNIEnv *env, jobject instance) { + return sColdStartLatency.getOpenTimeMicros(); +} + +JNIEXPORT jint JNICALL +Java_com_mobileer_oboetester_TestColdStartLatencyActivity_getStartTimeMicros( + JNIEnv *env, jobject instance) { + return sColdStartLatency.getStartTimeMicros(); +} + +JNIEXPORT jint JNICALL +Java_com_mobileer_oboetester_TestColdStartLatencyActivity_getColdStartTimeMicros( + JNIEnv *env, jobject instance) { + return sColdStartLatency.getColdStartTimeMicros(); +} + +JNIEXPORT jint JNICALL +Java_com_mobileer_oboetester_TestColdStartLatencyActivity_getAudioDeviceId( + JNIEnv *env, jobject instance) { + return sColdStartLatency.getDeviceId(); +} + +static TestRapidCycle sRapidCycle; + +JNIEXPORT jint JNICALL +Java_com_mobileer_oboetester_TestRapidCycleActivity_startRapidCycleTest(JNIEnv *env, jobject thiz, + jboolean use_open_sl) { + return sRapidCycle.start(use_open_sl); +} + +JNIEXPORT jint JNICALL +Java_com_mobileer_oboetester_TestRapidCycleActivity_stopRapidCycleTest(JNIEnv *env, jobject thiz) { + return sRapidCycle.stop(); +} + +JNIEXPORT jint JNICALL +Java_com_mobileer_oboetester_TestRapidCycleActivity_getCycleCount(JNIEnv *env, jobject thiz) { + return sRapidCycle.getCycleCount(); +} + +} // extern "C" diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/BiquadFilter.h b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/BiquadFilter.h new file mode 100644 index 00000000..20ee32e6 --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/BiquadFilter.h @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2016 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. + * + * This code was translated from the JSyn Java code. + * JSyn is Copyright 2009 Phil Burk, Mobileer Inc + * JSyn is licensed under the Apache License, Version 2.0 + */ + +#ifndef SYNTHMARK_BIQUAD_FILTER_H +#define SYNTHMARK_BIQUAD_FILTER_H + +#include +#include +#include "SynthTools.h" +#include "UnitGenerator.h" + +namespace marksynth { + +#define BIQUAD_MIN_FREQ (0.00001f) // REVIEW +#define BIQUAD_MIN_Q (0.00001f) // REVIEW + +#define RECALCULATE_PER_SAMPLE 0 + +/** + * Time varying lowpass resonant filter. + */ +class BiquadFilter : public UnitGenerator +{ +public: + BiquadFilter() + : mQ(1.0) + { + xn1 = xn2 = yn1 = yn2 = (synth_float_t) 0; + a0 = a1 = a2 = b1 = b2 = (synth_float_t) 0; + } + + virtual ~BiquadFilter() = default; + + /** + * Resonance, typically between 1.0 and 10.0. + * Input will clipped at a BIQUAD_MIN_Q. + */ + void setQ(synth_float_t q) { + if( q < BIQUAD_MIN_Q ) { + q = BIQUAD_MIN_Q; + } + mQ = q; + } + + synth_float_t getQ() { + return mQ; + } + + void generate(synth_float_t *input, + synth_float_t *frequencies, + int32_t numSamples) { + synth_float_t xn, yn; + +#if RECALCULATE_PER_SAMPLE == 0 + calculateCoefficients(frequencies[0], mQ); +#endif + for (int i = 0; i < numSamples; i++) { +#if RECALCULATE_PER_SAMPLE == 1 + calculateCoefficients(frequencies[i], mQ); +#endif + // Generate outputs by filtering inputs. + xn = input[i]; + synth_float_t finite = (a0 * xn) + (a1 * xn1) + (a2 * xn2); + // Use double precision for recursive portion. + yn = finite - (b1 * yn1) - (b2 * yn2); + output[i] = (synth_float_t) yn; + + // Delay input and output values. + xn2 = xn1; + xn1 = xn; + yn2 = yn1; + yn1 = yn; + } + + // Apply a small bipolar impulse to filter to prevent arithmetic underflow. + yn1 += (synth_float_t) 1.0E-26; + yn2 -= (synth_float_t) 1.0E-26; + } + + +private: + synth_float_t mQ; + + synth_float_t xn1; // delay lines + synth_float_t xn2; + double yn1; + double yn2; + + synth_float_t a0; // coefficients + synth_float_t a1; + synth_float_t a2; + + synth_float_t b1; + synth_float_t b2; + + synth_float_t cos_omega; + synth_float_t sin_omega; + synth_float_t alpha; + + // Calculate coefficients common to many parametric biquad filters. + void calcCommon( synth_float_t ratio, synth_float_t Q ) + { + synth_float_t omega; + + /* Don't let frequency get too close to Nyquist or filter will blow up. */ + if( ratio >= 0.499f ) ratio = 0.499f; + omega = 2.0f * (synth_float_t)M_PI * ratio; + +#if 1 + // This is not significantly faster on Mac or Linux. + cos_omega = SynthTools::fastCosine(omega); + sin_omega = SynthTools::fastSine(omega ); +#else + { + float fsin_omega; + float fcos_omega; + sincosf(omega, &fsin_omega, &fcos_omega); + cos_omega = (synth_float_t) fcos_omega; + sin_omega = (synth_float_t) fsin_omega; + } +#endif + alpha = sin_omega / (2.0f * Q); + } + + // Lowpass coefficients + void calculateCoefficients( synth_float_t frequency, synth_float_t Q ) + { + synth_float_t scalar, omc; + + if( frequency < BIQUAD_MIN_FREQ ) frequency = BIQUAD_MIN_FREQ; + + calcCommon( frequency * mSamplePeriod, Q ); + + scalar = 1.0f / (1.0f + alpha); + omc = (1.0f - cos_omega); + + a0 = omc * 0.5f * scalar; + a1 = omc * scalar; + a2 = a0; + b1 = -2.0f * cos_omega * scalar; + b2 = (1.0f - alpha) * scalar; + } +}; + +}; +#endif // SYNTHMARK_BIQUAD_FILTER_H diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/DifferentiatedParabola.h b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/DifferentiatedParabola.h new file mode 100644 index 00000000..c236147e --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/DifferentiatedParabola.h @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2016 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. + * + * This code was translated from the JSyn Java code. + * JSyn is Copyright 2009 Phil Burk, Mobileer Inc + * JSyn is licensed under the Apache License, Version 2.0 + */ + +#ifndef SYNTHMARK_DIFFERENTIATED_PARABOLA_H +#define SYNTHMARK_DIFFERENTIATED_PARABOLA_H + +#include +#include +#include "SynthTools.h" + +namespace marksynth { + +constexpr double kDPWVeryLowFrequency = 2.0 * 0.1 / kSynthmarkSampleRate; + +/** + * DPW is a tool for generating band-limited waveforms + * based on a paper by Antti Huovilainen and Vesa Valimaki: + * "New Approaches to Digital Subtractive Synthesis" + */ +class DifferentiatedParabola +{ +public: + DifferentiatedParabola() + : mZ1(0) + , mZ2(0) {} + + virtual ~DifferentiatedParabola() = default; + + synth_float_t next(synth_float_t phase, synth_float_t phaseIncrement) { + synth_float_t dpw; + synth_float_t positivePhaseIncrement = (phaseIncrement < 0.0) + ? phaseIncrement + : 0.0 - phaseIncrement; + + // If the frequency is very low then just use the raw sawtooth. + // This avoids divide by zero problems and scaling problems. + if (positivePhaseIncrement < kDPWVeryLowFrequency) { + dpw = phase; + } else { + // Calculate the parabola. + synth_float_t squared = phase * phase; + // Differentiate using a delayed value. + synth_float_t diffed = squared - mZ2; + // Delay line. + // TODO - Why Z2. Vesa's paper says use Z1? + mZ2 = mZ1; + mZ1 = squared; + + // Calculate scaling + dpw = diffed * 0.25f / positivePhaseIncrement; // TODO extract and optimize + } + return dpw; + } + +private: + synth_float_t mZ1; + synth_float_t mZ2; +}; + +}; +#endif // SYNTHMARK_DIFFERENTIATED_PARABOLA_H diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/EnvelopeADSR.h b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/EnvelopeADSR.h new file mode 100644 index 00000000..83fab9b6 --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/EnvelopeADSR.h @@ -0,0 +1,229 @@ +/* + * Copyright (C) 2016 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. + * + * This code was translated from the JSyn Java code. + * JSyn is Copyright 2009 Phil Burk, Mobileer Inc + * JSyn is licensed under the Apache License, Version 2.0 + */ + +#ifndef SYNTHMARK_ENVELOPE_ADSR_H +#define SYNTHMARK_ENVELOPE_ADSR_H + +#include +#include +#include "SynthTools.h" +#include "UnitGenerator.h" + +namespace marksynth { + +/** + * Generate a contour that can be used to control amplitude or + * other parameters. + */ + +class EnvelopeADSR : public UnitGenerator +{ +public: + EnvelopeADSR() + : mAttack(0.05) + , mDecay(0.6) + , mSustainLevel(0.4) + , mRelease(2.5) + {} + + virtual ~EnvelopeADSR() = default; + +#define MIN_DURATION (1.0 / 100000.0) + + enum State { + IDLE, ATTACKING, DECAYING, SUSTAINING, RELEASING + }; + + void setGate(bool gate) { + triggered = gate; + } + + bool isIdle() { + return mState == State::IDLE; + } + + /** + * Time in seconds for the falling stage to go from 0 dB to -90 dB. The decay stage will stop at + * the sustain level. But we calculate the time to fall to -90 dB so that the decay + * rate will be unaffected by the sustain level. + */ + void setDecayTime(synth_float_t time) { + mDecay = time; + } + + synth_float_t getDecayTime() { + return mDecay; + } + + /** + * Time in seconds for the rising stage of the envelope to go from 0.0 to 1.0. The attack is a + * linear ramp. + */ + void setAttackTime(synth_float_t time) { + mAttack = time; + } + + synth_float_t getAttackTime() { + return mAttack; + } + + void generate(int32_t numSamples) { + for (int i = 0; i < numSamples; i++) { + switch (mState) { + case IDLE: + for (; i < numSamples; i++) { + output[i] = mLevel; + if (triggered) { + startAttack(); + break; + } + } + break; + + case ATTACKING: + for (; i < numSamples; i++) { + // Increment first so we can render fast attacks. + mLevel += increment; + if (mLevel >= 1.0) { + mLevel = 1.0; + output[i] = mLevel; + startDecay(); + break; + } else { + output[i] = mLevel; + if (!triggered) { + startRelease(); + break; + } + } + } + break; + + case DECAYING: + for (; i < numSamples; i++) { + output[i] = mLevel; + mLevel *= mScaler; // exponential decay + if (mLevel < kAmplitudeDb96) { + startIdle(); + break; + } else if (!triggered) { + startRelease(); + break; + } else if (mLevel < mSustainLevel) { + mLevel = mSustainLevel; + startSustain(); + break; + } + } + break; + + case SUSTAINING: + for (; i < numSamples; i++) { + mLevel = mSustainLevel; + output[i] = mLevel; + if (!triggered) { + startRelease(); + break; + } + } + break; + + case RELEASING: + for (; i < numSamples; i++) { + output[i] = mLevel; + mLevel *= mScaler; // exponential decay + if (triggered) { + startAttack(); + break; + } else if (mLevel < kAmplitudeDb96) { + startIdle(); + break; + } + } + break; + } + } + } + +private: + + void startIdle() { + mState = State::IDLE; + mLevel = 0.0; + } + + void startAttack() { + if (mAttack < MIN_DURATION) { + mLevel = 1.0; + startDecay(); + } else { + increment = mSamplePeriod / mAttack; + mState = State::ATTACKING; + } + } + + void startDecay() { + double duration = mDecay; + if (duration < MIN_DURATION) { + startSustain(); + } else { + mScaler = SynthTools::convertTimeToExponentialScaler(duration, mSampleRate); + mState = State::DECAYING; + } + } + + void startSustain() { + mState = State::SUSTAINING; + } + + void startRelease() { + double duration = mRelease; + if (duration < MIN_DURATION) { + duration = MIN_DURATION; + } + mScaler = SynthTools::convertTimeToExponentialScaler(duration, mSampleRate); + mState = State::RELEASING; + } + + synth_float_t mAttack; + synth_float_t mDecay; + /** + * Level for the sustain stage. The envelope will hold here until the input goes to zero or + * less. This should be set between 0.0 and 1.0. + */ + synth_float_t mSustainLevel; + /** + * Time in seconds to go from 0 dB to -90 dB. This stage is triggered when the input goes to + * zero or less. The release stage will start from the sustain level. But we calculate the time + * to fall from full amplitude so that the release rate will be unaffected by the + * sustain level. + */ + synth_float_t mRelease; + + State mState = State::IDLE; + synth_float_t mScaler = 1.0; + synth_float_t mLevel = 0.0; + synth_float_t increment = 0; + bool triggered = false; + +}; + +}; +#endif // SYNTHMARK_ENVELOPE_ADSR_H diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/IncludeMeOnce.h b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/IncludeMeOnce.h new file mode 100644 index 00000000..bf6f1b0e --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/IncludeMeOnce.h @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2016 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. + * + * This code was translated from the JSyn Java code. + * JSyn is Copyright 2009 Phil Burk, Mobileer Inc + * JSyn is licensed under the Apache License, Version 2.0 + */ + +#ifndef INCLUDE_ME_ONCE_H +#define INCLUDE_ME_ONCE_H + +#include "UnitGenerator.h" +#include "PitchToFrequency.h" + +namespace marksynth { + +//synth statics +int32_t UnitGenerator::mSampleRate = kSynthmarkSampleRate; +synth_float_t UnitGenerator::mSamplePeriod = 1.0f / kSynthmarkSampleRate; + +PowerOfTwoTable PitchToFrequency::mPowerTable(64); +}; + +#endif //INCLUDE_ME_ONCE_H diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/LookupTable.h b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/LookupTable.h new file mode 100644 index 00000000..07423637 --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/LookupTable.h @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2016 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. + * + * This code was translated from the JSyn Java code. + * JSyn is Copyright 2009 Phil Burk, Mobileer Inc + * JSyn is licensed under the Apache License, Version 2.0 + */ + +#ifndef SYNTHMARK_LOOKUP_TABLE_H +#define SYNTHMARK_LOOKUP_TABLE_H + +#include +#include "SynthTools.h" + +namespace marksynth { + +class LookupTable { +public: + LookupTable(int32_t numEntries) + : mNumEntries(numEntries) + {} + + virtual ~LookupTable() { + delete[] mTable; + } + + void fillTable() { + // Add 2 guard points for interpolation and roundoff error. + int tableSize = mNumEntries + 2; + mTable = new float[tableSize]; + // Fill the table with calculated values + float scale = 1.0f / mNumEntries; + for (int i = 0; i < tableSize; i++) { + float value = calculate(i * scale); + mTable[i] = value; + } + } + + /** + * @param input normalized between 0.0 and 1.0 + */ + float lookup(float input) { + float fractionalTableIndex = input * mNumEntries; + int32_t index = (int) floor(fractionalTableIndex); + float fraction = fractionalTableIndex - index; + float baseValue = mTable[index]; + float value = baseValue + + (fraction * (mTable[index + 1] - baseValue)); + return value; + } + + virtual float calculate(float input) = 0; + +private: + int32_t mNumEntries; + synth_float_t *mTable; +}; + +}; +#endif // SYNTHMARK_LOOKUP_TABLE_H diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/PitchToFrequency.h b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/PitchToFrequency.h new file mode 100644 index 00000000..a06d3276 --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/PitchToFrequency.h @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2016 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. + * + * This code was translated from the JSyn Java code. + * JSyn is Copyright 2009 Phil Burk, Mobileer Inc + * JSyn is licensed under the Apache License, Version 2.0 + */ + +#ifndef SYNTHMARK_PITCH_TO_FREQUENCY_H +#define SYNTHMARK_PITCH_TO_FREQUENCY_H + +#include +#include +#include "SynthTools.h" +#include "LookupTable.h" + +namespace marksynth { + +constexpr int kSemitonesPerOctave = 12; +// Pitches are in semitones based on the MIDI standard. +constexpr int kPitchMiddleC = 60; +constexpr double kFrequencyMiddleC = 261.625549; + +class PowerOfTwoTable : public LookupTable { +public: + PowerOfTwoTable(int32_t numEntries) + : LookupTable(numEntries) + { + fillTable(); + } + + virtual ~PowerOfTwoTable() {} + + virtual float calculate(float input) override { + return powf(2.0f, input); + } +}; + +class PitchToFrequency +{ +public: + PitchToFrequency() {} + + virtual ~PitchToFrequency() { + } + + static double convertPitchToFrequency(double pitch) { + double exponent = (pitch - kPitchMiddleC) * (1.0 / kSemitonesPerOctave); + return kFrequencyMiddleC * pow(2.0, exponent); + } + + synth_float_t lookupPitchToFrequency(synth_float_t pitch) { + // Only calculate if input changed since last time. + if (pitch != lastInput) { + synth_float_t octavePitch = (pitch - kPitchMiddleC) * (1.0 / kSemitonesPerOctave); + int32_t octaveIndex = (int) floor(octavePitch); + synth_float_t fractionalOctave = octavePitch - octaveIndex; + + // Do table lookup. + synth_float_t value = kFrequencyMiddleC * mPowerTable.lookup(fractionalOctave); + + // Adjust for octave by multiplying by a power of 2. Allow for +/- 16 octaves; + const int32_t octaveOffset = 16; + synth_float_t octaveScaler = ((synth_float_t)(1 << (octaveIndex + octaveOffset))) + * (1.0 / (1 << octaveOffset)); + value *= octaveScaler; + + lastInput = pitch; + lastOutput = value; + } + return lastOutput; + } + + /** + * @param pitches an array of fractional MIDI pitches + */ + void generate(const synth_float_t *pitches, synth_float_t *frequencies, int32_t count) { + for (int i = 0; i < count; i++) { + frequencies[i] = lookupPitchToFrequency(pitches[i]); + } + } + +private: + static PowerOfTwoTable mPowerTable; + + synth_float_t lastInput = kPitchMiddleC; + synth_float_t lastOutput = kFrequencyMiddleC; + +}; + +}; +#endif // SYNTHMARK_PITCH_TO_FREQUENCY_H diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/SawtoothOscillator.h b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/SawtoothOscillator.h new file mode 100644 index 00000000..f27fafaa --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/SawtoothOscillator.h @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2016 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. + * + * This code was translated from the JSyn Java code. + * JSyn is Copyright 2009 Phil Burk, Mobileer Inc + * JSyn is licensed under the Apache License, Version 2.0 + */ + +#ifndef SYNTHMARK_SAWTOOTH_OSCILLATOR_H +#define SYNTHMARK_SAWTOOTH_OSCILLATOR_H + +#include +#include +#include "SynthTools.h" +#include "UnitGenerator.h" +#include "DifferentiatedParabola.h" + +namespace marksynth { +/** + * Simple phasor that can be used to implement other oscillators. + * Note that this is NON-bandlimited and should not be used + * directly as a sound source. + */ +class SawtoothOscillator : public UnitGenerator +{ +public: + SawtoothOscillator() + : mPhase(0) {} + + virtual ~SawtoothOscillator() = default; + + void generate(synth_float_t frequency, int32_t numSamples) { + synth_float_t phase = mPhase; + synth_float_t phaseIncrement = 2.0 * frequency * mSamplePeriod; + for (int i = 0; i < numSamples; i++) { + output[i] = translatePhase(phase, phaseIncrement); + phase += phaseIncrement; + if (phase > 1.0) { + phase -= 2.0; + } + } + mPhase = phase; + } + + void generate(synth_float_t *frequencies, int32_t numSamples) { + synth_float_t phase = mPhase; + for (int i = 0; i < numSamples; i++) { + synth_float_t phaseIncrement = 2.0 * frequencies[i] * mSamplePeriod; + output[i] = translatePhase(phase, phaseIncrement); + phase += phaseIncrement; + if (phase > 1.0) { + phase -= 2.0; + } + } + mPhase = phase; + } + + virtual synth_float_t translatePhase(synth_float_t phase, synth_float_t phaseIncrement) { + (void) phaseIncrement; + return phase; + } + +private: + synth_float_t mPhase; // between -1.0 and +1.0 +}; + +}; +#endif // SYNTHMARK_SAWTOOTH_OSCILLATOR_H diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/SawtoothOscillatorDPW.h b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/SawtoothOscillatorDPW.h new file mode 100644 index 00000000..a6eb4015 --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/SawtoothOscillatorDPW.h @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2016 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. + * + * This code was translated from the JSyn Java code. + * JSyn is Copyright 2009 Phil Burk, Mobileer Inc + * JSyn is licensed under the Apache License, Version 2.0 + */ + +#ifndef SYNTHMARK_SAWTOOTH_OSCILLATOR_DPW_H +#define SYNTHMARK_SAWTOOTH_OSCILLATOR_DPW_H + +#include +#include +#include "SynthTools.h" +#include "DifferentiatedParabola.h" + +namespace marksynth { +/** + * Band limited sawtooth oscillator. + * Suitable as a sound source. + */ + +class SawtoothOscillatorDPW : public SawtoothOscillator +{ +public: + SawtoothOscillatorDPW() + : SawtoothOscillator() + , dpw() {} + + virtual ~SawtoothOscillatorDPW() = default; + + virtual inline synth_float_t translatePhase(synth_float_t phase, synth_float_t phaseIncrement) { + return dpw.next(phase, phaseIncrement); + } + +private: + DifferentiatedParabola dpw; +}; + +}; +#endif // SYNTHMARK_SAWTOOTH_OSCILLATOR_DPW_H diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/SimpleVoice.h b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/SimpleVoice.h new file mode 100644 index 00000000..68e00038 --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/SimpleVoice.h @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2016 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 SYNTHMARK_SIMPLE_VOICE_H +#define SYNTHMARK_SIMPLE_VOICE_H + +#include +#include +#include "SynthTools.h" +#include "VoiceBase.h" +#include "SawtoothOscillator.h" +#include "SawtoothOscillatorDPW.h" +#include "SquareOscillatorDPW.h" +#include "SineOscillator.h" +#include "EnvelopeADSR.h" +#include "PitchToFrequency.h" +#include "BiquadFilter.h" + +namespace marksynth { +/** + * Classic subtractive synthesizer voice with + * 2 LFOs, 2 audio oscillators, filter and envelopes. + */ +class SimpleVoice : public VoiceBase +{ +public: + SimpleVoice() + : VoiceBase() + , mLfo1() + , mOsc1() + , mOsc2() + , mPitchToFrequency() + , mFilter() + , mFilterEnvelope() + , mAmplitudeEnvelope() + // The following values are arbitrary but typical values. + , mDetune(1.0001f) // slight phasing + , mVibratoDepth(0.03f) + , mVibratoRate(6.0f) + , mFilterEnvDepth(3000.0f) + , mFilterCutoff(400.0f) + { + mFilter.setQ(2.0); + // Randomize attack times to smooth out CPU load for envelope state transitions. + mFilterEnvelope.setAttackTime(0.05 + (0.2 * SynthTools::nextRandomDouble())); + mFilterEnvelope.setDecayTime(7.0 + (1.0 * SynthTools::nextRandomDouble())); + mAmplitudeEnvelope.setAttackTime(0.02 + (0.05 * SynthTools::nextRandomDouble())); + mAmplitudeEnvelope.setDecayTime(1.0 + (0.2 * SynthTools::nextRandomDouble())); + } + + virtual ~SimpleVoice() = default; + + void setPitch(synth_float_t pitch) { + mPitch = pitch; + } + + void noteOn(synth_float_t pitch, synth_float_t velocity) { + (void) velocity; // TODO use velocity? + mPitch = pitch; + mFilterEnvelope.setGate(true); + mAmplitudeEnvelope.setGate(true); + } + + void noteOff() { + mFilterEnvelope.setGate(false); + mAmplitudeEnvelope.setGate(false); + } + + void generate(int32_t numFrames) { + assert(numFrames <= kSynthmarkFramesPerRender); + + // LFO #1 - vibrato + mLfo1.generate(mVibratoRate, numFrames); + synth_float_t *pitches = mBuffer1; + SynthTools::scaleOffsetBuffer(mLfo1.output, pitches, numFrames, mVibratoDepth, mPitch); + synth_float_t *frequencies = mBuffer2; + mPitchToFrequency.generate(pitches, frequencies, numFrames); + + // OSC #1 - sawtooth + mOsc1.generate(frequencies, numFrames); + + // OSC #2 - detuned square wave oscillator + SynthTools::scaleBuffer(frequencies, frequencies, numFrames, mDetune); + mOsc2.generate(frequencies, numFrames); + + // Mix the two oscillators + synth_float_t *mixed = frequencies; + SynthTools::mixBuffers(mOsc1.output, 0.6, mOsc2.output, 0.4, mixed, numFrames); + + // Filter envelope + mFilterEnvelope.generate(numFrames); + synth_float_t *cutoffFrequencies = pitches; // reuse unneeded buffer + SynthTools::scaleOffsetBuffer(mFilterEnvelope.output, cutoffFrequencies, numFrames, + mFilterEnvDepth, mFilterCutoff); + + // Biquad resonant low-pass filter + mFilter.generate(mixed, cutoffFrequencies, numFrames); + + // Amplitude ADSR + mAmplitudeEnvelope.generate(numFrames); + SynthTools::multiplyBuffers(mFilter.output, mAmplitudeEnvelope.output, output, numFrames); + } + +private: + SineOscillator mLfo1; + SawtoothOscillatorDPW mOsc1; + SquareOscillatorDPW mOsc2; + PitchToFrequency mPitchToFrequency; + BiquadFilter mFilter; + EnvelopeADSR mFilterEnvelope; + EnvelopeADSR mAmplitudeEnvelope; + + synth_float_t mDetune; // frequency scaler + synth_float_t mVibratoDepth; // in semitones + synth_float_t mVibratoRate; // in Hertz + synth_float_t mFilterEnvDepth; // in Hertz + synth_float_t mFilterCutoff; // in Hertz + + // Buffers for storing signals that are being passed between units. + synth_float_t mBuffer1[kSynthmarkFramesPerRender]; + synth_float_t mBuffer2[kSynthmarkFramesPerRender]; +}; +}; +#endif // SYNTHMARK_SIMPLE_VOICE_H diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/SineOscillator.h b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/SineOscillator.h new file mode 100644 index 00000000..6a1c55d6 --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/SineOscillator.h @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2016 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. + * + * This code was translated from the JSyn Java code. + * JSyn is Copyright 2009 Phil Burk, Mobileer Inc + * JSyn is licensed under the Apache License, Version 2.0 + */ + +#ifndef SYNTHMARK_SINE_OSCILLATOR_H +#define SYNTHMARK_SINE_OSCILLATOR_H + +#include +#include +#include "SynthTools.h" + +namespace marksynth { +class SineOscillator : public SawtoothOscillator +{ +public: + SineOscillator() + : SawtoothOscillator() {} + + virtual ~SineOscillator() = default; + + virtual inline synth_float_t translatePhase(synth_float_t phase, synth_float_t phaseIncrement) { + (void) phaseIncrement; + return SynthTools::fastSine(phase * M_PI); + } + +}; +}; +#endif // SYNTHMARK_SINE_OSCILLATOR_H diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/SquareOscillatorDPW.h b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/SquareOscillatorDPW.h new file mode 100644 index 00000000..01587152 --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/SquareOscillatorDPW.h @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2016 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. + * + * This code was translated from the JSyn Java code. + * JSyn is Copyright 2009 Phil Burk, Mobileer Inc + * JSyn is licensed under the Apache License, Version 2.0 + */ + +#ifndef SYNTHMARK_SQUARE_OSCILLATOR_DPW_H +#define SYNTHMARK_SQUARE_OSCILLATOR_DPW_H + +#include +#include +#include "SynthTools.h" +#include "DifferentiatedParabola.h" + +namespace marksynth { +/** + * Square waves contains the odd partials of a fundamental. + * The square wave is generated by combining two sawtooth waveforms + * that are 180 degrees out of phase. This causes the even partials + * to be cancelled out. + */ +class SquareOscillatorDPW : public SawtoothOscillator +{ +public: + SquareOscillatorDPW() + : SawtoothOscillator() + , dpw1() + , dpw2() {} + + virtual ~SquareOscillatorDPW() = default; + + virtual inline synth_float_t translatePhase(synth_float_t phase1, + synth_float_t phaseIncrement) { + synth_float_t val1 = dpw1.next(phase1, phaseIncrement); + + /* Generate second sawtooth so we can add them together. */ + synth_float_t phase2 = phase1 + 1.0; /* 180 degrees out of phase. */ + if (phase2 >= 1.0) + phase2 -= 2.0; + synth_float_t val2 = dpw1.next(phase2, phaseIncrement); + + /* + * Need to adjust amplitude based on positive phaseInc. little less than half at + * Nyquist/2.0! + */ + const synth_float_t STARTAMP = 0.92; // derived empirically + synth_float_t positivePhaseIncrement = (phaseIncrement < 0.0) + ? phaseIncrement + : 0.0 - phaseIncrement; + synth_float_t scale = STARTAMP - positivePhaseIncrement; + return scale * (val1 - val2); + } + +private: + DifferentiatedParabola dpw1; + DifferentiatedParabola dpw2; +}; +}; +#endif // SYNTHMARK_SQUARE_OSCILLATOR_DPW_H diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/SynthTools.h b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/SynthTools.h new file mode 100644 index 00000000..807b709b --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/SynthTools.h @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2016 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 SYNTHMARK_SYNTHTOOLS_H +#define SYNTHMARK_SYNTHTOOLS_H + +#include +#include + +namespace marksynth { +typedef float synth_float_t; + +// The number of frames that are synthesized at one time. +constexpr int kSynthmarkFramesPerRender = 8; + +constexpr int kSynthmarkSampleRate = 48000; +constexpr int kSynthmarkMaxVoices = 1024; + +/** + * A fractional amplitude corresponding to exactly -96 dB. + * amplitude = pow(10.0, db/20.0) + */ +constexpr double kAmplitudeDb96 = 1.0 / 63095.73444801943; + +/** A fraction that is approximately -90.3 dB. Defined as 1 bit of an S16. */ +constexpr double kAmplitudeDb90 = 1.0 / (1 << 15); + +class SynthTools +{ +public: + + static void fillBuffer(synth_float_t *output, + int32_t numSamples, + synth_float_t value) { + for (int i = 0; i < numSamples; i++) { + *output++ = value; + } + } + + static void scaleBuffer(const synth_float_t *input, + synth_float_t *output, + int32_t numSamples, + synth_float_t multiplier) { + for (int i = 0; i < numSamples; i++) { + *output++ = *input++ * multiplier; + } + } + + static void scaleOffsetBuffer(const synth_float_t *input, + synth_float_t *output, + int32_t numSamples, + synth_float_t multiplier, + synth_float_t offset) { + for (int i = 0; i < numSamples; i++) { + *output++ = (*input++ * multiplier) + offset; + } + } + + static void mixBuffers(const synth_float_t *input1, + synth_float_t gain1, + const synth_float_t *input2, + synth_float_t gain2, + synth_float_t *output, + int32_t numSamples) { + for (int i = 0; i < numSamples; i++) { + *output++ = (*input1++ * gain1) + (*input2++ * gain2); + } + } + + static void multiplyBuffers(const synth_float_t *input1, + const synth_float_t *input2, + synth_float_t *output, + int32_t numSamples) { + for (int i = 0; i < numSamples; i++) { + *output++ = *input1++ * *input2; + } + } + + static double convertTimeToExponentialScaler(synth_float_t duration, synth_float_t sampleRate) { + // Calculate scaler so that scaler^frames = target/source + synth_float_t numFrames = duration * sampleRate; + return pow(kAmplitudeDb90, (1.0 / numFrames)); + } + + /** + * Calculate sine using a Taylor expansion. + * Code is based on SineOscillator from JSyn. + * + * @param phase between -PI and +PI + */ + static synth_float_t fastSine(synth_float_t phase) { + // Factorial coefficients. + const synth_float_t IF3 = 1.0 / (2 * 3); + const synth_float_t IF5 = IF3 / (4 * 5); + const synth_float_t IF7 = IF5 / (6 * 7); + const synth_float_t IF9 = IF7 / (8 * 9); + const synth_float_t IF11 = IF9 / (10 * 11); + + /* Wrap phase back into region where results are more accurate. */ + synth_float_t x = (phase > M_PI_2) ? M_PI - phase + : ((phase < -M_PI_2) ? -(M_PI + phase) : phase); + + synth_float_t x2 = (x * x); + /* Taylor expansion out to x**11/11! factored into multiply-adds */ + return x * (x2 * (x2 * (x2 * (x2 * ((x2 * (-IF11)) + IF9) - IF7) + IF5) - IF3) + 1); + } + + /** + * Calculate cosine using a Taylor expansion. + * + * @param phase between -PI and +PI + */ + static synth_float_t fastCosine(synth_float_t phase) { + // Factorial coefficients. + const synth_float_t IF2 = 1.0 / (2); + const synth_float_t IF4 = IF2 / (3 * 4); + const synth_float_t IF6 = IF4 / (5 * 6); + const synth_float_t IF8 = IF6 / (7 * 8); + const synth_float_t IF10 = IF8 / (9 * 10); + + /* Wrap phase back into region where results are more accurate. */ + synth_float_t x = phase; + if (x < 0.0) { + x = 0.0 - phase; + } + int negate = 1; + if (x > M_PI_2) { + x = M_PI_2 - x; + negate = -1; + } + + synth_float_t x2 = (x * x); + /* Taylor expansion out to x**11/11! factored into multiply-adds */ + synth_float_t cosine = + 1 + (x2 * (x2 * (x2 * (x2 * ((x2 * (-IF10)) + IF8) - IF6) + IF4) - IF2)); + return cosine * negate; + } + + + /** + * Calculate random 32 bit number using linear-congruential method. + */ + static uint32_t nextRandomInteger() { + static uint64_t seed = 99887766; + // Use values for 64-bit sequence from MMIX by Donald Knuth. + seed = (seed * 6364136223846793005L) + 1442695040888963407L; + return (uint32_t) (seed >> 32); // The higher bits have a longer sequence. + } + + /** + * @return a random double between 0.0 and 1.0 + */ + static double nextRandomDouble() { + const double scaler = 1.0 / (((uint64_t)1) << 32); + return nextRandomInteger() * scaler; + } + +}; +}; +#endif // SYNTHMARK_SYNTHTOOLS_H diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/Synthesizer.h b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/Synthesizer.h new file mode 100644 index 00000000..e9e1e949 --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/Synthesizer.h @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2016 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 SYNTHMARK_SYNTHESIZER_H +#define SYNTHMARK_SYNTHESIZER_H + +#include +#include +#include +#include +#include +#include "SynthTools.h" +#include "VoiceBase.h" +#include "SimpleVoice.h" + +namespace marksynth { +#define SAMPLES_PER_FRAME 2 + +/** + * Manage an array of voices. + * Note that this is not a fully featured general purpose synthesizer. + * It is designed simply to have a similar CPU load as a common synthesizer. + */ +class Synthesizer +{ +public: + Synthesizer() + : mMaxVoices(0) + , mActiveVoiceCount(0) + , mVoices(NULL) + {} + + virtual ~Synthesizer() { + delete[] mVoices; + }; + + int32_t setup(int32_t sampleRate, int32_t maxVoices) { + mMaxVoices = maxVoices; + UnitGenerator::setSampleRate(sampleRate); + mVoices = new SimpleVoice[mMaxVoices]; + return (mVoices == NULL) ? -1 : 0; + } + + void allNotesOn() { + notesOn(mMaxVoices); + } + + int32_t notesOn(int32_t numVoices) { + if (numVoices > mMaxVoices) { + return -1; + } + mActiveVoiceCount = numVoices; + // Leave some headroom so the resonant filter does not clip. + mVoiceAmplitude = 0.5f / sqrt(mActiveVoiceCount); + + int pitchIndex = 0; + synth_float_t pitches[] = {60.0, 64.0, 67.0, 69.0}; + for(int iv = 0; iv < mActiveVoiceCount; iv++ ) { + SimpleVoice *voice = &mVoices[iv]; + // Randomize pitches by a few cents to smooth out the CPU load. + float pitchOffset = 0.03f * (float) SynthTools::nextRandomDouble(); + synth_float_t pitch = pitches[pitchIndex++] + pitchOffset; + if (pitchIndex > 3) pitchIndex = 0; + voice->noteOn(pitch, 1.0); + } + return 0; + } + + void allNotesOff() { + for(int iv = 0; iv < mActiveVoiceCount; iv++ ) { + SimpleVoice *voice = &mVoices[iv]; + voice->noteOff(); + } + } + + void renderStereo(float *output, int32_t numFrames) { + int32_t framesLeft = numFrames; + float *renderBuffer = output; + + // Clear mixing buffer. + memset(output, 0, numFrames * SAMPLES_PER_FRAME * sizeof(float)); + + while (framesLeft > 0) { + int framesThisTime = std::min(kSynthmarkFramesPerRender, framesLeft); + for(int iv = 0; iv < mActiveVoiceCount; iv++ ) { + SimpleVoice *voice = &mVoices[iv]; + voice->generate(framesThisTime); + float *mix = renderBuffer; + + synth_float_t leftGain = mVoiceAmplitude; + synth_float_t rightGain = mVoiceAmplitude; + if (mActiveVoiceCount > 1) { + synth_float_t pan = iv / (mActiveVoiceCount - 1.0f); + leftGain *= pan; + rightGain *= 1.0 - pan; + } + for(int n = 0; n < kSynthmarkFramesPerRender; n++ ) { + synth_float_t sample = voice->output[n]; + *mix++ += (float) (sample * leftGain); + *mix++ += (float) (sample * rightGain); + } + } + framesLeft -= framesThisTime; + mFrameCounter += framesThisTime; + renderBuffer += framesThisTime * SAMPLES_PER_FRAME; + } + assert(framesLeft == 0); + } + + int32_t getActiveVoiceCount() { + return mActiveVoiceCount; + } + +private: + int32_t mMaxVoices; + int32_t mActiveVoiceCount; + int64_t mFrameCounter; + SimpleVoice *mVoices; + synth_float_t mVoiceAmplitude = 1.0; +}; +}; +#endif // SYNTHMARK_SYNTHESIZER_H diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/UnitGenerator.h b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/UnitGenerator.h new file mode 100644 index 00000000..3b5a597c --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/UnitGenerator.h @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2016 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. + * + * This code was translated from the JSyn Java code. + * JSyn is Copyright 2009 Phil Burk, Mobileer Inc + * JSyn is licensed under the Apache License, Version 2.0 + */ + +#ifndef SYNTHMARK_UNIT_GENERATOR_H +#define SYNTHMARK_UNIT_GENERATOR_H + +#include +#include +#include +#include "SynthTools.h" +//#include "DifferentiatedParabola.h" + +namespace marksynth { +class UnitGenerator +{ +public: + UnitGenerator() {} + + virtual ~UnitGenerator() = default; + + static void setSampleRate(int32_t sampleRate) { + assert(sampleRate > 0); + mSampleRate = sampleRate; + mSamplePeriod = 1.0f / sampleRate; + } + + static int32_t getSampleRate() { + return mSampleRate; + } + + synth_float_t output[kSynthmarkFramesPerRender]; + +public: + static int32_t mSampleRate; + static synth_float_t mSamplePeriod; +}; +} +#endif // SYNTHMARK_UNIT_GENERATOR_H diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/VoiceBase.h b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/VoiceBase.h new file mode 100644 index 00000000..b77b637b --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/synth/VoiceBase.h @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2016 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 SYNTHMARK_VOICE_BASE_H +#define SYNTHMARK_VOICE_BASE_H + +#include +#include "SynthTools.h" +#include "UnitGenerator.h" + +namespace marksynth { +/** + * Base class for building synthesizers. + */ +class VoiceBase : public UnitGenerator +{ +public: + VoiceBase() + : mPitch(60.0) // MIDI Middle C is 60 + , mVelocity(1.0) // normalized + { + } + + virtual ~VoiceBase() = default; + + void setPitch(synth_float_t pitch) { + mPitch = pitch; + } + + void noteOn(synth_float_t pitch, synth_float_t velocity) { + mVelocity = velocity; + mPitch = pitch; + } + + void noteOff() { + } + + virtual void generate(int32_t numFrames) = 0; + +protected: + synth_float_t mPitch; + synth_float_t mVelocity; +}; +}; +#endif // SYNTHMARK_VOICE_BASE_H diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/util/WaveFileWriter.cpp b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/util/WaveFileWriter.cpp index 5744d8f5..9e8d37d3 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/util/WaveFileWriter.cpp +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/util/WaveFileWriter.cpp @@ -17,10 +17,10 @@ #include "WaveFileWriter.h" void WaveFileWriter::WaveFileWriter::write(float value) { - if (!headerWritten) { + if (!mHeaderWritten) { writeHeader(); } - if (bitsPerSample == 24) { + if (mBitsPerSample == 24) { writePCM24(value); } else { writePCM16(value); @@ -46,7 +46,7 @@ void WaveFileWriter::writeShortLittle(int16_t n) { } void WaveFileWriter::writeFormatChunk() { - int32_t bytesPerSample = (bitsPerSample + 7) / 8; + int32_t bytesPerSample = (mBitsPerSample + 7) / 8; writeByte('f'); writeByte('m'); @@ -60,7 +60,13 @@ void WaveFileWriter::writeFormatChunk() { writeIntLittle(mFrameRate * mSamplesPerFrame * bytesPerSample); // block align writeShortLittle((int16_t) (mSamplesPerFrame * bytesPerSample)); - writeShortLittle((int16_t) bitsPerSample); + writeShortLittle((int16_t) mBitsPerSample); +} + +int32_t WaveFileWriter::getDataSizeInBytes() { + if (mFrameCount <= 0) return INT32_MAX; + int64_t dataSize = ((int64_t)mFrameCount) * mSamplesPerFrame * mBitsPerSample / 8; + return (int32_t)std::min(dataSize, (int64_t)INT32_MAX); } void WaveFileWriter::writeDataChunkHeader() { @@ -68,22 +74,20 @@ void WaveFileWriter::writeDataChunkHeader() { writeByte('a'); writeByte('t'); writeByte('a'); - // Maximum size is not strictly correct but is commonly used - // when we do not know the final size. - writeIntLittle(INT32_MAX); + writeIntLittle(getDataSizeInBytes()); } void WaveFileWriter::writeHeader() { writeRiffHeader(); writeFormatChunk(); writeDataChunkHeader(); - headerWritten = true; + mHeaderWritten = true; } // Write lower 8 bits. Upper bits ignored. void WaveFileWriter::writeByte(uint8_t b) { mOutputStream->write(b); - bytesWritten += 1; + mBytesWritten += 1; } void WaveFileWriter::writePCM24(float value) { @@ -124,7 +128,11 @@ void WaveFileWriter::writeRiffHeader() { writeByte('F'); // Maximum size is not strictly correct but is commonly used // when we do not know the final size. - writeIntLittle(INT32_MAX); + const int kExtraHeaderBytes = 36; + int32_t dataSize = getDataSizeInBytes(); + writeIntLittle((dataSize > (INT32_MAX - kExtraHeaderBytes)) + ? INT32_MAX + : dataSize + kExtraHeaderBytes); writeByte('W'); writeByte('A'); writeByte('V'); diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/util/WaveFileWriter.h b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/util/WaveFileWriter.h index 05392f8b..e3cebce4 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/util/WaveFileWriter.h +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/cpp/util/WaveFileWriter.h @@ -15,16 +15,18 @@ */ // Based on the WaveFileWriter in Java from the open source JSyn library by Phil Burk -// https://github.com/philburk/jsyn/blob/master/src/com/jsyn/util/WaveFileWriter.java +// https://github.com/philburk/jsyn/blob/master/src/main/java/com/jsyn/util/WaveFileWriter.java #ifndef UTIL_WAVE_FILE_WRITER #define UTIL_WAVE_FILE_WRITER #include #include +#include class WaveFileOutputStream { public: + virtual ~WaveFileOutputStream() = default; virtual void write(uint8_t b) = 0; }; @@ -56,6 +58,10 @@ public: } /** + * Set the number of frames per second, also known as "sample rate". + * + * If you call this then it must be called before the first write(). + * * @param frameRate default is 44100 */ void setFrameRate(int32_t frameRate) { @@ -67,25 +73,49 @@ public: } /** + * Set the size of one frame. * For stereo, set this to 2. Default is mono = 1. - * Also known as ChannelCount + * Also known as ChannelCount. + * + * If you call this then it must be called before the first write(). + * + * @param samplesPerFrame is 2 for stereo or 1 for mono */ void setSamplesPerFrame(int32_t samplesPerFrame) { mSamplesPerFrame = samplesPerFrame; } + /** + * Sets the number of frames in the file. + * + * If you do not know the final number of frames then that is OK. + * Just do not call this method and the RIFF and DATA chunk sizes + * will default to INT32_MAX. That is technically invalid WAV format + * but is common practice. + * + * If you call this then it must be called before the first write(). + * @param frameCount number of frames to be written + */ + void setFrameCount(int32_t frameCount) { + mFrameCount = frameCount; + } + int32_t getSamplesPerFrame() const { return mSamplesPerFrame; } - /** Only 16 or 24 bit samples supported at the moment. Default is 16. */ + /** Only 16 or 24 bit samples supported at the moment. Default is 16. + * + * If you call this then it must be called before the first write(). + * @param bits number of bits in a PCM sample + */ void setBitsPerSample(int32_t bits) { assert((bits == 16) || (bits == 24)); - bitsPerSample = bits; + mBitsPerSample = bits; } int32_t getBitsPerSample() const { - return bitsPerSample; + return mBitsPerSample; } void close() { @@ -138,13 +168,16 @@ private: */ void writeRiffHeader(); + int32_t getDataSizeInBytes(); + static constexpr int WAVE_FORMAT_PCM = 1; WaveFileOutputStream *mOutputStream = nullptr; int32_t mFrameRate = 48000; int32_t mSamplesPerFrame = 1; - int32_t bitsPerSample = 16; - int32_t bytesWritten = 0; - bool headerWritten = false; + int32_t mFrameCount = 0; // 0 for unknown + int32_t mBitsPerSample = 16; + int32_t mBytesWritten = 0; + bool mHeaderWritten = false; static constexpr int32_t PCM24_MIN = -(1 << 23); static constexpr int32_t PCM24_MAX = (1 << 23) - 1; diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/ic_launcher-playstore.png b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000000000000000000000000000000000000..3e81c2c0232b49c6a5d570891b8cf847d62767d4 GIT binary patch literal 31684 zcmeFZXHZm6zb?8MqU4MSk|oChiITG*sHlJ>B`YEbNFHX8AUR4H$&wTVBuJ1bAUP{J zN=9-VU znYx-Rn0H|4Re+kZNvjp|+M%nzwOy)Pes-6);!HM8^45u7LDuQPoB7k#IP3I+muN4= zwtdt2ROXI_hN52ovU@uYy`3r33I4{Ls^V}nj%s)z*|W?EcuEu+{*kz9DnDJIk8{IS zX*`2ua@dPvk7rN7jbYvGcxF=mT>$u*sHEu3Nqc|eh~$kr@Q;Tw7c&b4GXZ}ktiB9j z`7W@Qj```r4Y-O?A*z$lvA=)aNqTV9If~tk6<#*}EdBJ{J%`>lpRbn_GlU`UBtaRU zyerQ#)!}j+`tVpvr&>D0gMGzZ2b#BK_a=&TJC7Zp%7rLiTcWby>XbfLTV>R~DGrPf z);XyLeo`FP538k9e5n@UT0!9?<@62hS@h;{zuh4LRs-z%45Cv#(v-Li-~IeOuZnXQoeAJQ;BuB>p^^VwJ$ zn^>j)rvLF|j)=PDx~WhW33m1Bae^vFsYzI^zG8uTx4O|i;4uP{u8f(N5yk(U9qx9zbwb2-Y5ItF;DEj_ax>){r7|OFQ>`>u_MTq6Bvi* ziTi&2UH!sv-=6o?$RC%yO0%^1cheCP*!EwK$IGHtYug0iy|I(>!*9#_r_3Td_QP&8 zNpr2-+On|HV7PI9Q1jxXkYygc56TsiD#x$bzZG{e+f?n#?W>ruFY8?KlaTp;xIC^4 ztuXhtTgWrVOiHx+fP6$i*}aAO=@?mg<}3fry=o-`BmeJ)Fa|f=sMpchkk?JbbJ9(5kK3Cz zjrs$xXRkZ2nGUOg*Rpx}NMUYE&BNBx`^Rtx|1t_n5Aedo+|uSgQoK_2wTuI5`{z^OX`1lvuBWf5c*bxVr%Pe>)#Mp5>(f5GuwQ4clF-xJv0v>@c(1|`?Y~D2pm4m z&BMCp&zv_rcKRZ3#vG5o`l)coqwyKv?wBPJeD3BJ;mny?+LdSQ9J>P3beN%w!_2s= zeWbymOOc|^?ov2`_o-3(or!h-a%Be0P?zQtGo6z$rF{DANAQd5$WX@UCYI#nT?#FVLF?|}{p_g!X8h@$j?Ip~BqI2W0j=TxAAQ5`n{`FJIjC4t7)x2-?3SmZ*^vxrak#PHkttjJ$aNyJa$*8wx}qM$?i{9>mOjUs35km zlgx~IQlBY?X*d-fmO4CmyNKa=)^PaX06Ly`1#S~S>{F?t#?&U-?i3Qs5sjcrC1sXM z2SzA(Aj0RFSCOY#&yAp&1~kB#lcrqKjY2Oii8=swxR--U0?5xY!(*| zfpEKkqCyI0ub1ANv3V+2eS7hjb?2T1{+($TxQ&qFUtCBaj7o7wx^ft{SlhIJnGAr= zY&fkr8$C=XH`C{;)gRXCrIA}HZ|15;Yqbt6ba<|O`>aUKIJ#U_r*G@hTSIx$C^ZaV1Kc$YJ6N`vs&{L%iai3chz z#TB*n(v31zG;gIi=h3%<@bN(x6i9PtPZCj$h*m`rDn3^ zJJsVvAUOWb0uaeN6P|6WAn(3=YK|-Nb7_c|O4%A`vzgkQk7KGaOi+K2L1ZP^&@U-3 zdwLN;9;wqE{Y*Xf4zt_&fvxWarvAR6a0dO7rc_1?ryB@jbRvj4+=#f!e*c*bP zB|%peKdUGeSKz|vMQIaRm6_Ge((bmk$@Vm>-(${_O z$;B|`Kr?w(o+bKM7C-F}6F18_j?ooy?){Ud(RddHfYPrk2)$oh{ER=7{9JKTt<0ZB zClH>2+iL$QJGLJa|4mVBy-1GEH@fCZ{eDFP_4`arvSu7L4;V2(pzsL&AUPtqPLH%#~4CUN|eAe zJZn5*KJy$+q7=)+V;{r4AK8=6;JU4|0*Nb))Tqj2e)=8T;=RcS?H`H|uSwe|+GyJ7 z^^hQzJ~rAugjOAGuU--Nw#y>+H=5A${=@fOFg{b-(}$7R;Ft?&fC-{CERkX{ zlR#O+t5?^H()Y?~+HUx{y=xokdJ$_eHoI-$rEu{SlJh{wz3YO9z0__jB5CR-^@jso zo(zb)m^>DOz5FB|iO(6Ig|DVcM~($HoWKhZ>2Ag$g>JM}PJr{FQM@;Hx2fMp=FP+# zMn7Y}WD6#;GDkCM%!a210vzcOErVS*KS9~#%#J4wzdt)8ZNA0isHW;(eoIhD&$@18 zXY~O%34)IRNYWlh+ek8hsUy{@_yj7bjJfS*o0L1u>9$E;yK0-}2Krb3kJMj*%)#Y(4ijU3iL+v9}TfvNV zmBP`9(kT{6;a=^m^7oN2#z`;UVkWFX6&l%JW*@w9`ukK%Mkzsn8krY9H{~A*XQL^M|r_h+@ z#;app7iBBH_5IP{MmcLgiPLo_CMRa6bPusUym4YZ49&w`mH_adxTI+w=>3VcQqfj? zf=!j$w)E_a)%q6`Rck%2HU6Nh!DXq`2m~O&7pN&gm)hvuIz`yCi`?n|e=$Mfg>Q?MQs>H8^F^ z>8E`j=w30~#+dqJoJf4U2@`844xJgXZFqDk>F9+`g#Yi|?b zva9k-jf!7}Imgl8=#@;hH76*h7Ab`Uo%W*IPf!-vt=o&%?TG3QWgm9drOc%yW1_TR;-n;rruT)q*<$ydty|j&D|3esKV(=-%Ln4d&H6er)A&O`Oy&u!i@ojy4<^-)5g~+Kl_())xP{3|FoLO zO6jS!0s-TdAH?r81A7RrT%ia!jcC%ZR?+~nMy9us1yxeMfXv0HfEvKYMUX4i z;NpTOW;#Hw6jYH8+y#iK0kBcp%SQXkpO_s^N&M27z7q#M%rFN*jRpuoV_%ILhHG3)@V=lj{togxPMG@FAYW=9946wT+Q<7+G5!te#6w{T@b4#w^ZoNjy+mI))B z*6-ubXCXpf6JtF z?xvU_A75!D?dYPT(mqMrT}x^fizqb-ntNeV9N85A-ed_uwYgYRJk1y&bMBt^*{AOD z5aU7Ttnr%jX6*L3_khElT9?|8ZFuqJ0yO;Sx42V4TvPFY^G1AI+}A;;-B6#ZIf43# z^G{|>M`J={Ck8j*atzD^J@x@oAqPKUPz>lka(bif)j=;Cm)ri){}#QY8`7n|TyS27 zYT(g~#@Kh6P%CZEa_vIRIQmvft06vo%PFr{zRrJ)f=0^$khHcNM)`Owhna00wFyLMGjRr{&G4+ zEJsv@z5Kh5+1?ndgH?1mu`c(W1Pb(S*5)GN%C}^wquk|srdtWuj>3(Q$g$)HDZHgR z9Es1`XO4EX>U3OUbPY9>{t#o?wjgCoW`17X9RYeDtKV3-eJ<1K5hhT@E&LNVm=OcB z8~`Tbemx5(vL{D2#nK;F6uVa9!P^tLBIV0H-~C;wnl+NP9drMm zttleU6<=8NJKP=tPXR{y1`QHB{1!T^#7{;Xt|$ji4g%Vwi00js9WnN3BDqPMS#FMU z>8nOqwuD18PppAbb9SnoEM{xmO4AmF%jV@`siO`9H#;SQ9S-d9fA#CA5GYWwh-fS) zvR$vsZ8({l79h1Jnw1rn@!x09jCHnp4l9XnapXR}Q$>wsi^H}Sbc>cdLbFpm)TX*% z>mckZ=Aqmw0n)cn%W5Rm_| zwsp3_qEJ!hwxzLYj$u`iri0A@FP1ID8rS+(nk|$46Okf)5PgB?gZn+;O#nPg4*=0f zcfKZ_%xtg$_=l!4$qfx81}`a({y6-+!{QN+yx}omS0G_}eRYw#GQ;gx#~wD#4mz`{&9MjLME8$3C6c1|dlMEEZL((T~XWsLs5myR|DX^vf(BU%Oi0wB@Ze-q!C295EIh1h7Fw`GNSQ1vr#kkKn*MH92@0{R*{J@rB3M>=4V&~ZFxOq9o zNt9HKLYmUF6G~!F5i@xOPn(>5lYJ+A87adh>*)3Aad=o%elf!veh4Z(Aa>oARd!#o#;3$d@ZO6_@F}`V^Py8y(nHqz0@=~i zP~DRAqml=w=ymJ)^VNqQdrRM0~N<{&zsk@kEZ6lBFP`snyOaE#INuw)L={1U5WQbsLf_Q^+ETX6?C>^f^>!|0ah z^hYYiT{1B|=V-(GZLnaq3ljTuJ%icKvfwA(4m;cuC~GwAr20yy6v%~IVS=r+V|`?M%I zD@^kgb3>B5Li&WqT3xdxYwf`MRw^0aW~#d0%SH*SQg~NBEG}3T2{kwRx@SZME4ufU z&ZKBhv&hbS0G^`TBKw%<=kozS-}0*jjSeKD22%LWX!#^g1k}aBD_ow_;;UyZ&r53y)(!=B z1C_v(r7iK!k9Xt@AJ4{m$?bFomP+N2Os@Y?Wzz)Sn_J^1Glm*siL#fUI2wGWxd3)4 zoB;Q+j=FH~K?)qc%&rRk#MT|gI5{oC2x{u-?vp1bIu_#~ROfl#dpy76o;zZqZ?{oP zPw#OL?^-tM3vp$(+8Ng%ms{O>!lZm^=#8@LxGV};gbL)hz9F5VrY#HBm@7Iv-+8)P zaQznjMW5Gs0TNZ_;G*IEL9miiLK??$f>pqRd_?%YvE4h3$A*Bq5 zOg9??_EuVZ0awI#!9Fcsb zY}hO@SLMfg{xS>Hz^kreIf|~Tyb0^$sf8O`jmVYV=sKEXTy~8n z>Wg^5)%-|%*VhoYB)+sS<^mrQZp%*SYEwu1mX6On?<&v*m{Jf z(Zv|FAbl{yTH@-jrY-in^qiARvAAe&C|A{ePHM{zF?fk)3s|=ymr(BOxD2t0SvJvI zA3)YgCm*NbjxejVNT7jo*Hhh~Bj)D!>bHa1gRNE;tB!*zz3FQ*RgNd`Jze(l%HA`u z7b2>pg;Sk)dDF^KrG>+>&uLSC&kDo^q~V#vf3GN;Tty52d5GfP zBS{N|H+?pH)-;j}EW4WmJ?c~6@klNMjjJI%Yeg?ZUaw6lPIinW5%KHaaZ zDJ1s$Z;J$d{Hel08w5zNvRz313i$t3G{RN*w+RB{MET!sC+We(oyk8f+4>0s$>6I^ zCuHVU8|{aobL;NAZr^(i4v*xjPPCU%1U4Yqr;-HlADeGhjhqWVTC8QDyEaYOZ9cuC zwX8>tAo|BJQz;yJ_D%oHNm^4pRWJc9zuzg#e7w-(c2csCWmf1_rM4&+CrG6=VC8L^ z6V}-sBl1iN`ogVWRm9uD7vzV;)37)nW@8vP%NA#RtHsbO(A7a~=u-D7bXc=cBc4cp zoGY}BxX26ak9`0KfQB^Q+I*1Q^(th4ZJ4yJD}mCoWj7dlI_d|_7R(lT*=lXo8FYHY zx?SKa=U3;~t6-)C0zD;WWi#b8<>_(vZ(S>)_;=D=;JQWsNIQ~m>AStDEeHh&H^A!H zaxB^s`7o8QzBex(*XtB&|JF?H6qpC!%FP;5x9^Jtv98`aUmL|B6>-LnbIO= z_7*1GX0;oWT*Wj&EvmE6M0Mc2&`HO4- zR8Icqp2^+ z3MJ+NTwTfl&@P~vzN`xuya$$Q?Fg?9?HB&dTGYOE;c^jm5pt1r)27AK^ncZle1QK} zZ7+x1EiOj0ch6EY+)S?3>Akf|0^|yz`1VwIB$&H@9@=1Wy=OdrbP&%@qD<} zdO?o1X@C4ag%iB}ND-{I6gCK2hwXatFKO;9B7oS(*e8B(ysLTm@izf^e(X)2Se|_z z-vB*pP z=v#{Zs6uPSA9ODNVnjL*6c&NgY)gdI&~c!-u+n2&51p@z9`b9o7)iEUB)U>U9@+A^ z5;c$9#hPU@jwvIe9B~}oFz6_OSjyvV$_@w7hCgZ-wcIU>*Q0w z=NKb4ND=`D%*j^{GyTBOHH@HBwP~-ebg5%SKoC?vS{918-ro35?|YMk?QX(zj5hK%9O!aC zOCN2(bFgtSaZL=64-;mandg}2oadhBMagauTuLyFn!pw>6)DsAp{z-Lu$7x6q7d2P z!D|$WhDkgU-f?u4>ePtEWL*H`-K>cjS`B>PAa}xl@|4Jl%8A;E#);M`0c~2rb-9e7 z+-oo)CN86O>>!Y^F+5bfrA|r0)`$M~7`b(~VB+u=?u=3y2Vg5&c6}_0{j9AK)J#gF zs-v2sIww#yvX`^Htc^ub)09Rp>EUn_lyrW=kh+0WYq1_)-cGBX)QEUnUci>UjAo43 ze(9wyr4)|t5+g|DX>;p1plbgSmXv`-IJL!;1=HS>PkTXc^bXskzDZ^dr}BA2XT{UH z+(O24hF5ujI|@bh*4`{DR&=H%d+E?4`=q!rZMBHdnc zkhzwng0+=(n04vv*S{j1i$8c|o?XAOh@dnmscwo|&Th?X8aVGEq<<}}k0F&@om^Tq z%HB*%uYaGqD4?JcYc@Z`wOB^KWTj9w+V5_}%C^fkG z>|0S@tdHA+MEgg%Oq!$|O}5E2l2lQqCu%1{bxFL~q3Fl9RI?;e=qPM_`3L+|TB6!7 ziG!0u|9;PI+!^f|*~Mby=we%U>S{_O+{*SX%M|X9FPrOeP>`cXK8WUYo{k6&F~oA_ zP1`lvO4=IVVlX_^)cf*7SOut}kfN;GxzBMaiDU@i0P40qFup16SK(p331{OS-D)Bm zGhA6bS84XEs>Zs=f6rumQjfKoLKjmwbMQ5|{cvk>YjtRF`0g5#t`F>g5O87+#d64(Ww9KIIjiGh zKWGMr2`CKXsmWA40?E=z#XD`;h-l~D{jb&zK^dTwqWw5RGYIqwL>)1=xOQYhPLd8Q zwy$rQY^7{XZe8Ctks9N&$93Xy;&tM4TFVM?*-Zqh=%ggC#$NXI63czhs2;O%uctsHMnJL#Y8l85x@8tjfILwPJ}Xs8HXi@O@v)w z`#V^+^n?mjQ4=}xUUxZ%b3nZSfPWNtXZL5-QZ>M=kaw#y4SKSJKJ`JCqXlU;G>TfSXOO+ROVXh%!<>o1OWBRX-hd;=DKTL*ocD`|b9qw2E z!)E3`hQ(dYVXPpirlSU$ywwWTdek=5NYz278d;0DldO|ZC8NazC~?0>499#?rb(4} z8cW#cpqz5y<0&L15m#cn^7+e#&w577$jgofbO-UqIq8M*^EqelcF1`gr z6_XTm)wBs&4S<_|C9$VDsO7q(Ke^Hes(J5uA2-=8j0E)mUB5|J1j@b5_$xr8eP_K+ zjC@M?E~DPGZ<3c5dYbm*w?>OR<6Mtzc4}&>o}yJAIAm*AqX& zKsgBFB^oj6@QW4dcBS@sx7Mm9cR zz*%(}j%`vzcuK6qM$ud%Qzt}?o^cCRmVaAzz3r&&bi;GzFB{7PmTKY|p|M}S32kc? z(fF*?4v4G`%B|E7abXXmE!*%eJW`|8N`_ZgW2LH<&(W0y`_Or{rOhHDZyoui-Q&-M z9NjOqAm^!Q8S(+cM7H`{Uv?R1%cie!y&OK$CT5SH+zwAvU#mX%B0rf-U&T)I?fv6b z(A2>`;9QmFG=cY5mdW;oOjvv}J2r-?92ZHmH95H7UYtOEHh#S&RaL$dG;tiGyWyRC zl);$L*i-6LwJKXb;o8WRzv}Iz8lszHk81Brf&|lEp(_Q^C-G^7e;H9|67`84h07dQh)1E)z-WG zCdS^lY>~d!ikffm;#wHWqqFSk$+XTPRx-ClY!@kE*^(oY;dT6@qy%3k>kn_W4w55Y zXJ9E^Gho!&dgzFoU^v`Ei$R6m@3CZGHM-u7lJC3>|#%JroK(`gTv zTg_bTjUW5a-&3Buux{iYBYpHEL}R4nHAj=$u1P(i{6Bvde1(+eifR@AlGER@>*d(V z;OntTrg;hSIYXUo-6Y$(6p}(0JIVC`@^Sy;(Z|z-Nwjjn#3`)m^QpLAJS=uz$~taQ ztLXJxC5S)HvYSPL_HEvH`MUS4T6MQyzNlEO?~GUX$%}0^i@a%p>XkFxHSyGtapTbH zwq}LzMq@Y!d`Q3|Iku_M=bfPwp0DOn|gL zk^P)tHpRDxg`K1RHj-im=;USK%{tCHZzk;cA=$iT-Bls~P6 zZmjz8A9ASWFy6)(7XnUngqp$;l~n1z5XT5p2@D*4fkeC3@FXXsU9m4>iDm-=MX@Lp zl$g!lv4WV%|HM@(Q0_5@XsXt`fV52_>NBEdxVrNv$P-7O);GGSc9*3J60efG-vpNG zK)i$zmuF(-Djs$;m$)F^I_S5(pC%l#-3Hz$;3QAKwUh)clcyE&S_yos%42@+I5poq zyIJYu68TC^C*g%#)pMN0wsC+MVmgq%){u+R5;WiGf^HV@CI|H`VxKQ6IkL`o7vbc5 zp*rI)BaeG^mdQ6p4~d zF>#-O3>aR{v5_sA(FaxF4W^o+#^q1^Q&IIs6mkz|!zwL2E@!J3#-&5+Gq%$>0KRy(( zLrfB!)L8C+uWgcTu5ErGKjc%n>6X=(3L$R}YCso5cile_dsIs1-AKI9MDhU_6CZ=X zHGc8l%I>_gM-hjUD)}I>y|=YCj7pmA-;8hyQNZ!_3h)Z_dh2Bieu>J~XchH~33S49 z!gnHY3a$=kzkV^gI}ilTI=EFjf)2fRN#)gK?M)8-RH^|S5w@*ASoCIzPp7WUT=U(+ zMd6_cP=qM{YlQHq2xzc|FBYIVqFyVmdDy~Y2O7%+p4K&?~OW;LU)-~FNTG1JiG@7WmXd%#Y zLx^CF0j-W_B!f`%dcDI)jwD>2b#Q3zcYrtPm0jLa-`A+$4&!IO@)Wz@knepJsl%@; zf#SGE1dr+gN2i7}H6j#ici(Cx7I(l7cBgp*EWSZY1nAy-6PnbLWreSHvf1or@SAM* zA-H8$7LY=mZIvh5ppP|p&w$EMgt|SwK!h^oL9})1wgomb>NHrqQ4x59!%U)GpetS) zfvM3LYX%!@3H2{1RZNjLg*TFoyZyVPOl zq~8ZX?l_1wJ9s2Lpr51%lQ(Aq8ksqgaae*3`Im|1a(?BuEzdJL2P$RVx|WH#DSitA z!mFkdzuGX?+zgJ%nh$>6G7}f#hZuelnYmcVN`Ev)dC$46S2lr)HD(;iCXIY7JRU%i z>(Mo+R#c)!`I{TF<94Sa<5qeZ-xZGXgic>#~<5Pt8}n4Ri* zwgQOEVu`0iW1p$VSb9H-2=n-O8?TV8wtl{mxVD{(iYfnH@R8vnBK0V0ieU6>kt~4? z$k>Y;HinJ(54bo_At-(e8*^Q#@?cHZch6CnjCKSh&|f4kZZ0pO#X$!3Y}F;1-|MSF zVZm-EoO!Q(L^Xuab#20B1> zKpa_4MmUabb~wMoE(H}A)e4loXWPTnAo^1Smi@# z8`#c#Z?{D-c*tweBGh`;L?WHGH1JoEYHPi*ie&M(H1+h zeedrY6X%28Lz#ox)AB$_`p*~u0p97BKSnv6yHE6rt>3#Czs!+}wg;8E0npm9Qdc6# z>kot)^w%Fxnm*OZXeMw}x);a;`Hnj1r?sVWleq`BhGYYHJ4=g+eKqat^paJIk6rsY-Xg78ns^0Jp^yM}v;5 z!Co*j<7ouHxxfD?8;2@CG076DV0%)ydT_Pn`HEYv-WIc9JjJUXZh5YspJ!T?9-$mS z^LL6ZHNrZ1)Pj^R>1p zWnDIXoyOWl-bFt;OuOwhrD;6j})1}R@du6(TJRG{N|PCO`W zsnh~x2oK{er8nAzcSMTlNXPY9{M=}h3`^>g__9&m53Rr8uGEag5`l;qXqq2;V1Xpa zVZna+DfVf_P-miyH*}jRP5YbB7io`)ra|a@^sK@Mt!@#%{a1JK9<5sZuq&fQWBMt3 z1U!b^M>I;$9jPE^!}3H@A`F@%UIcZ2C!~(X9Q<^R^9{Ak!x}9ET)o`P9i7o z3B+!a&98wDC+PFiUQh+u%P9`OP<_3Hz!RZyqT);J!;;a48*R*_G#$6$DHsG@UbpH} zf;x3NxA7U>dRTYzJHO4_dK$E1yNoFVVsG9EqrYO~Q7`2b!$f8r{L)0vn-09L$6W+P z6F!^-A))Y+G?*96s)BQgBowC!w!d-%SGrjk?63P=F_I z{K67T(+NC5wPj*nP;S4`JqYkd^?>+SJ&T~B#%<0LWO8&AbrOmV4WVp;)6{DiQ9E(X zO+8?*>g#RK=?4O`!^7gC=P!pD34?dz6_^#xuAG@f-u$|_+n1|0Z74*wx!n%BYLNSh zy?e5I-$7RhbltY3%M-VlnQEuD7IMMDL2#c$@*ldSb$RY1K(G@1s>%?0OWX{kI1JwpT=@{P2OdPITXF~Ma8#+h|V%gboSa(SR)Ir^K?i; zK(34_Ax2us1iIvD#tqigxt2P0XvkZ2IJb@<1)U?Maa zk>|{)6FhJSL6V&{w@+Q?B(vy4?=@D}V_|b<*fkgfj1e|@iq6F3CqPBFhychTvS9>u zbzG#iv~!=I&i8498?uF8(+VT6GV(0febYI%{}lC|EFp2}b8Oy#CCGf2Y>C?VyKt7l z9ut8AhYkhkujYZ>DrHVexqzYVKpaPgiZ4nuN~UYIJb@hjk1WAK1U#y4n%NfI2^#U-vbxZn~G5dutu3O!pd;1A4G_r{HR zCgb%y$3s93(lUx&uT4NSwJFR%yPE&Ky})192sHDD_rT>Jfox`9nEMuEWUu&m0+XGc zRa=iam_&PO%Fr~2g@RoH#ge)ExKExonbO4O-M6Y_x2Xlq*9kYK9(RpJvs64`jw*If zIk0saS#Qz2)KxYhPiVz|AJxels&Or8fc_iyRoPQ+p!zF`qd(DXfWVU__|?ZB!R0jm zjLFn=U?K0uJhdcdP^{V{dnVt-AODr*5PjDLGSs(#KOiz}D!6YItL0cgh#VNVHE>1g;c86@k>JQqW;~kw=@rgm2^r z?;bkL@18nhXR_V$VoHj&3ZZ5q0av9D2e1zoR~x+`evo6J-RbM>bR?kImL5T3sFY};3o`2`lEID&A)nL2_ymywvQUG5e(jRZD zS5{D6tLg5`7+@Tb9568$&Gt8tG75P@C{K&J?o4>@d&{{bu{tw)3oo?)c={200^PgL zay=i9c{{>}BS)eJ4AIy5>_YM(LbH+0^Un*;3(uECTx6@jyi^Y*P}Af8@YLXexD&i? z^uzFEv4&;;K5trBWj7aJzPHnPu8+r>3N$bj5k3T;{HxzTy3IQvk)qz>hkKlL zUj*gLhkvl{PP9xu6e6;7?tVcw-+P_T6sSJPAfHjGLskVqw~jqm8cTUTTw|0BB>Cz{ z{xewul{73he*f=&7&8KBr8VK-)kUD|8YRTu>0*v2wnWzhRV3VDj<@aey3Se32l2^N z3BNZ+M$fbzdawB0G+WrcMvlc;cT-$;Jh>;x?=3Az*YMGS$0KHWs|gyA4G+#Ct)5)( ztTm>);}#8c+?H6TGQq^C^AKR|7e-L_`y;n;1Di0l&n+UR^6H>~v_yL5Ti33}zMmub zeb#Wrzd+JG=u+~!g9`*O8y!ez-eTnaxqAGVsY&ZGPEuZ2caL1fbLjz9p!nQsMug5< z8qYz99_}7{vik#3Zr>9|p>qGE#2&Cr75*{jnS=WD|Yd zGRNj-tP0VXEj5#WLb|l_)fcT#?&avR>t7NU>SKqw#YM3yQEv&W&}gSzU#Qingl6;8 zU6g7GDuF%baJ3kGJcUiC{oRgjpNP8N?4AkTw35KS2MBgy`QyhuGwmswjnnfS` zQ*z1HzN(kAn)-1=PV=<&)u&_7JAR&~*WGqHe8uA3#;RNLcKaXCH2$D%m&1{A5?@aT^Rc%-5mZx0_AMbgxBq3L|;ICV#vD^Mpz<2Z6P`U7d zCIhJZ-`Y1c5h`^(vQ*9Pt$`jNMrITT{m-L%er{#^WdfcWb!e7Gij>~wU8%2S89et< z_dfTir}sGbRAoXR1igUu3ay^_RI&XKnkzrf8Xk-&5G1Nu?bb(Xc==ieU0p4npy=M0 zLweVnouSjshKA4!sbnV@H?&1nLDi;3{b%_<3Fiq53D0Cs7QJTBE=Jc5eKt0=HIlwa zei3=##?^b^va|ki9OmSaY*zSbr!!|5Qmwx6O6?eY89_}r<{d*3R+n${8Pf$znI`yd zr^)8A6}o-t#$w8_)zbFxLd1K0%R4PvJ4>>yRxP!c|P9a+>ZR z=2)E9jO~p6xp^uqwpdF5s!4V*0>(6+HSEG&&GWB4=hZDjenU*rSox6}^>ON1s6q9t zC1`!@)`_+|#|y<5u0#3Qfy4JR!qqAARVf>kWi5*qF>I)WNV!z+#DcpC#r(*SwTE_XSF)4^*ji6WW zmEPXjVD7Z~i3&)a3)vxDsg97@rcesfSHP5S!`T1W-$&y&Ll+JRIO?Aw#=$gJzXnPG@v#l_UH1Q5cCoprwhL+zh&69TAokY;!V^AOjU`HgSr~|7k=x=XfxD7y;}Gn3^Dh5yMC> zLiK&8wLzp9_B|aiX4LQmBk>&{Qx-~uKRt?1II=v|^BFzKG21x*B{W`F7$ct+P6nz> z=HFXIpkT2MJzeqRY>(*fiTUkCWC_A}>t8>jDW3_E8wC~nq7pi`5+Rtx;KjQKRw9PW zA^)Xkau@|H2Ks{Rvh!sy(WHa1Up&>N?nvBwfxMr_BLz}Xcgzf#X73tSq&~@ z+;ko=v>l5S;*ds3ngrf5wWPIrZPKWYlVwHD5Jb^T2>N~ft~#|ow0Xh~+)+$ZomOR5 zGgC`bn|3|728FtdL|=EBeMEN$AxB-WVDcfGB5+NrR2?+ZvB$37@ACBZaeK724Lrjs zMkFe{U{NFI3qnRo{!syQ-S6Mjg~^52g-Z~#Kupc}E?C(bejsOS+CV^=NihCO06lcc z(tD1&UQOkSf?jfS#6G0SVkdh#KBQr9dA6^d^%nw|y9GFZ%@G_!3A;0abt$Ruya3sNR=_!)NqPR6BAq}*j{C$t&cPymYmTXU(~F}| z5YmS;k28-qPcR>18wtZSMBWYQTj)^uy$RZ!!@AqVH#@7rv<_uBgdC^(3kj^IO*`Dx zM2#XyBl$vr3Gxc|`cu~%tpcX($iZd^=ZWV@=cA&^!yy;Sff%6zD5zzl^Q1*5rNl+^ z?|>49EiHf;t_PKXM3N`qK2)X%qQ-nhn@YFJx4(CI&Fq%BG5S?QJ6PXh+hoK2=eiLsVoYZo}k5OIf~nfV0`aK){`P0FIx^D ziIsh#`Asl5#+TP&R zVd-R&2fk!vPW=}6P7sz6Iv>ykTI$7Un)c_707zc1lM&Xlo7nDBYJqBT#5&yK}Kgm756wLU_|`9hkxGEX&kPkse%m4bs%E`V1Z{!_Cp7cLUh^4#a{S z(v0$x8)JEP$Dcm6lcahH4jxUQou?6tu-Hi#Ol0a8Zs>Ik~K;2$;J z2ih=|zEczjaWOKl*1@x0KPk8C$Owdiaw6ImJP7_rJKq@%XV7qnL&jbkx5z&KD zCyj{+BBDh%g6O@ss1YrQ-tI&YM2je6Fr!PfAW>%+B}#O{hzY)HWIgxue$RTJKkr)K zkMCWNf7YJ0_qE%3p8Gh@d`Gz{1ww4sjF{NKwXEzUSD_gxH?cRz3zWb zlw^S-U06e=WOaMqf%w`^0i}D==o_)yXm6X_WDlU#@T2`F@82Im^;6l2R=wP>2^k-I zd*J~e8u|t8VSUn$KE;!|aF)bNK)o3>o-=`_kVh@);+Yv;&x-`4)JfUG3?Ioug8LL{Wx>)r4628p zY5tQ(5ecJ!aIPUrq{vkj!Mp-%MHaf}sY4bH_Un7q#_tbv!g3M6e7m(}@w-5j_x+J@tEn_qs3?vCSX!Sdxvw6-B0S_$smn-CGXg;nYsjn$&f5x-b zwI$rgF~=jiwT`xz`74;3{{oN6p3Vf7XB5oYH7p(JC&(RVodoGHa(t#hCOMZ2wL<-I zI#)VaG#lBqkhMdDbsaW6*}lEAK;F1X0_r!X(7)#64k zvOl0@hPZ2XX6Y+-y|w89#bBx@`mAq@v>QOfHn7CPt6T>@ucan>#EUjBj?e8XRsU7< z=^DpGD7lNpn|q))*d;F_P+Q@1pw)QyTQ>`1UBUrDmdsH*qvsVPC$47V7EsAdZF+bP zOryt+`85aBZX z{^Hv~wb`VzsxwrjbjvV^{@C;*s5FLrMCKA^AgNQx3(dbID0J&u6))u!Di^F~M@W8n z;&Z>50jGNUW!U1nYiP87eDXK%j`he<=EJO*6o$SQ^=I-0;P~ ziH>xXF+8)R$OfbeoTAO<+gqIb<<{2Y`C*FB*WMA2ouoU-Jp30fjCGlY_|%rc4|?YXw|~frvUri7Dg2`$nhDfb5U&15 ztrV0M`dcpZ|Ba+-6jta8Dw|IEG&fEBKInY7-7On5?mHr8H1OyXFYMmS zUmIPZzQrsEl%JB^b-5Q4E7tu=Vp$Fhfu-kzDM+bC65uo-8New_uNV>(amg zBuVh>6V4Z4?jrs!E6|=TG@nQF0t#xH$sO}YF_CY*7*k;T|Ax#6Nu?wPtu0L#6yL=IvE&)lFDaU0S+^2RY}eoO8A<7V-DYF^8=yM zmEUeEAIuh%X3|a)oa#+KjRWrn5jf1UhIc?A3vW<+4W!gXg4|74IYW?iqifBhbv=40 z_Fd@1&C&iea#HR=6cAw}8j4*`AG^&E4;B%(Fr}oAtp@7UfK~0#OI}9w;FE;?VC)SP zeXW1DXTlHt-nZt~(a)f51?c>byM~QmKTy%V=NIv!S=|X z1|}Ryf5~Tw>dVc#nv{WDoY~m-AD*~@7+j!2xi+;tA{DeJaD!7J(TDB{<>D*X!7Na0 zUvDa?d!PnJEE5Hoy3|}StlDhrkdzl9y+5fj}F=&1s(!hxudD&c?F zSa>Qj%*Tc2W8{L%-|mhUMD^LqWhR^HG#GRFScMF7Tg!p8m@H{ZN0AfdvqwO-zv9Lr zREKh_{$0<|!HsKBiJ9nqv-8gbp9f9H=}O99)?a93=SJrJ!LhGWVblVo^0ZztOE9WT zs{z)2!}Mw!FxlUtHYEoZsRvM;(%M>8qPde+UTfXkB4PYKWyK-s$t|5*RH z#r-Sc>pq)jRl{@6hh#i!u9K5=5g%0~e`^1?co8>`q}Mv<{mLr95-5&OuWk!H>!K9-SVmGZnotKeOO}4xB=ti0Uz)Nu+zl$6&gEqG zdi##_Vi1<8lI*({5_<_G;hu=9d5~dNZVcynotR0-rg!89i&;pCyE`R7@9`Bq0ahC@2_XzhNvW`kEv;O*py|UxzTwRj{Nc-Uwy9Sx zb%E-nvFd0A8WeAiI z6^qQg{KHzB>MKekU)7FLSFIepYJ25;x1=oBKq5}OHF?M(wl2u*YH71ZvM^fn!s)uh zrW?8YeWD=EeRTgK%1ZH3ZX#%t;z?Z0V;mv+qY|gF`O86P z_kvGK-8Z{c)``PhlmmjUUH>97e(hJ?;^n`UVuMQrPH&-=MO2RcK&9A2FR3qjlScK; z{K&;DJHwraHTM%rQ^_Fq^T*B`{l{nP=<_n$hS)12F) zoQr$oquSU)pZ-iPohzQJ9LZ@NnXyc*(n8M`2luFsHjHz6#7BEoqWl@mGLgA*(WQMpbBK~v*JiSMvC@L*Mcy*;}pv!<;XRA-MbbnD(_Qs5k zpsTJIo~rbC&rv;qU@C0i?0Y%=eeIpp4F;e6u3a&~wdtK?nc2edT_m(Y``+l1Yfvk- zSx%)IL2&iW#9Q3X=vSNCrJgdqApG}kzpvXkR7=03|GZSZa4c2lB}Gk6h1iR!E$NmH z5}lm(vA+B6FZ-j;ECn03s`jbXBTu=rE|hHAV7dOg?`$?B`x5Cu2Pi(>Xy6FrJ|21- z#~|}z?&I9RW2M3(FU@3L+FO4KSP+^jfKAtT0EW}@nna;f7feZhJ&9Xf)*`;uI6{uz zU4OrN{Vp84_4k~X+|^n; zaT%4z#wnS@^70^eGUzeYjib@YYp{Ko3mH{8-K%rn+7DGK`E-Mcs>15%ppo@4BZy(M zQZLQ~q2Kh_dGWnn?G{1c{c-PCg8fX~JvXg{og|rnz2Uu5LGLk>Nw0|#CDiInphlwS zg>rFrD&A&mS?NXxm{~n8j;-^WVtkT6R+_xUI8u($eexSh-U$|lduI^2B3Q>X@+!Qgk*h048}uJi29LGC%6bGq{C zHo+M7xc*tMkQn=zn!Vn{7L$HM-p=xZ(pHFq{a$n9V5FkKa;~=9guOcdN>}1gT*&Vw z&5%2AY7E!LwW_VGw8`gB#=PnuvhE#nUdaLqtwip z78CCq2u6yInIlSjR$ry1B5CARM)J>x3qH)72J zGE3#>Ai59N`(MoN_0Q~fNB!wK4WRWp7@jh6NUmj{Oy3p87WbQ`tvrXHE+eGG$?P}V zL7MDRG$tN8=uCfB_`#pA8I^ws6LZr_gc#{sx9yBPMrW-&OnBoorxXLd$$)2XqwPpu zxre(sJQH4dG6Hl6xXZnE&9z9b>s#vh-RuP>J=;LESQNcNPmt`l@l6$bM`OS z2_K}XE7#}LT#MF?tywZtQTcM-na6c6OyvT6GMBd!9u?l&U*=PA93A?q17&h8i#_Ro zlM%S@2OW|-j0?zWEbGphv1ps@S>iOg~apX~DM2V2?uL9$MC@e>|z#kfnmBKk%M z4StXx@$q_-`v+$^D2p2FdkdR0(q4pmDXZge5yWCvV_~AMGtZ=q)1P;}H+kBY_P&N} z$F3|YR9~Jfw^hz3C?)P{xeJXkOC2k!BGcuJ*8<9S50x-Fi6F89QE!0mkZ4Ah#pQ}c zUXLTA?_O(=IIo>@{JZ0Vw|!H&A2T%6h;`BsN2{sF5D%>yZdsJ+^wWE3ZKLX4eRh@4 zyua4d-`ApxPwI5MR&z?EL*}V{qJq3Pi&f`AUv`6gv4(FmMgV5CBs$fm#pYTd6NC)4 zH{ASoCn1IvHMHwzGH6TTTv79yUUux=8|l5B%s8)a!)hpN_>j|B#z#44%Hv=C*0M!5 z<#VE`VMtgb>^TM1jmS@vbU5waFL0Uj*R5~n#5L!St76kDpXo0t&nNkB3bO|S`S(T>q8Qu=iIwv5go8eE?WJ0G>Z@j{(6x{1xENommwS*K}b8`1rQofM64p4q#3MOQNm- z=cz)T-)$rz!|~)d?p!hj!B_xJc7gA#7>I;~)}b4H;BuD+O4X+jvts4^cw`*;z~t43CXa- zK1Jd(jaKd64B==`={Mnvq|egOyv+;joXZx0(WGA4llOqjZ!^{^?Znp`?#ft!=fI$$ zQz_ZPa>q^Kdk1;e-gL~Hl_0;S<3!Kki9Gg6Rz;7eV3TWy9Ym+Gzq5DRfuh2p3Na9K zM0E4{GZ_~vKa(*_e&?sP)oB6_swS;U9&@h;l4Lf9H9!)w=%GbY+ngQK7Q@EvgDbn2 z8h&2#9XVF~&T~^?4b6^%avIgNMk-bHtB}Q)v{q=p%{TAoltd3{UNdn759jz-@6@r3 z4Oq%!xRm!c{Gfgz!Ki|`_*-OpowioQ0pWdo`6+rbv2tfb&|$VsV}#&VMvPMbvlmBx zrVZoN)A~;kvhOR+ReEM?x=BoA{d1=7%+Ok5%4%c^ zEemoE4%?%0>=)k39u9}D(uLgTEwThEAOp3qr{Auw!0KeSl~pE&?5*<|%|4C@TzY}q z6}CC@9Zbyo>?|0?5MplBYs_wQX)|+cLXdjHVr*{Z{o-YL;Z(IhYYoF(dX|5`&R{u)UPoZF(*GyyKhbxuT1Hu%+MphrI4k zGM6*Apgv4Cji|9gMKg23)KsH9`yc!p3ZN<(>AJkER7RK9&8Uw|ymgF_*)41A9t<*a z696R5CGXJU(@wSVm^mx$+LO!4T7+#B*MeSR?c?Y+ZugP{Zi5ca3UHm--aqI!Q8Y5K zrf`lSa7mC>C)yn_?+UgYxAXRJ>Eum&D{5_rh@D7^A$uA5$>qnSFUe{#`dbG6__>4} z+)7+u5gayh*xV>}2tY)mtSQl=rz!-;=WQwQ7qXfCkA)O=QwoaD)Hex8%D9s9Tu@hr z5am&)du#i39QAo$XUH2i>&21hfv2LBQ=hBBFoEFSYAPi=sl`f;V^=l`=!6pIMC}+s z_RrHJnUOe$VJGlIDrnA-i_u(sz!r{a^obx9=;PZ3fc`^^F4!+*>(}_>E1I5OQNZ1{ z3?^f7>DNz?jQ(ypyQfEJa4D5Uj7{_aYo3TQB7pIgu)@(tFQ$;|;TW?#Hx42nr8vBO z#(_jY!g~+^%sd!Jn0J{ybV{ycYuF%bL%3XO&;i~e&tN)2HG0;<&20wV3(K?nJ@*;5 z3}>Y}Z2X~S?oX0W(}ytB@6otZF3|OI;p9Tuz}+fBQ3K6$?Ye#WscERJ@Ou6unW^K% zAMs=_Q%{p&KXamAe2I&2V^Wq1@Y-i^_wC9X;Q}X$Y{eN|Nmme$G8a9S;9RZ)nz`(C zcAzVFde#Y$EU9lbnID+~=dcbgAXFnZFFda7n!5svV&xUZsU(w4u9@mqo5%stQO(`s z^nQkr>m1}Q=R=Y0I49!obry1xsU0>U2MDbj-tGr_1s&YAb6PRCfwu*>&3g~nz-_yK z@ulNt2EMw*=4+1E@24irz|(Yf@gj4lmh;2hxhPVq1d8MVx=hC$4i_mEjza0YrTyU> z`@^%Wz*Cyr<~TaY!4kE%PD)<(Xkh#cH^xq$JhI%g(QEYz>-)nElE9ELEu}ld!nt0# zbpO^owQ^E!AlsNRBscB)I7Y$V!Ad&p;gNf**h2*3_xl-qv1 zNzf|_>N+-ljWTiGSMrR6(%i^IC1A3OW2jsUe9>95UNOV@<5zd3M^$AnD7S5?2E7Zx3h^>n6iHC_mC@wI%55D52T7PYno{!Z>7#{VI zl)5mV7JwVth*^c1X#De{!d|7n1dW3F-vYLf{%-s`+ed0h#tbt@it=#>rT&*@tTO%ML%W;f#%Z2fpg|ocUEdXPtVnV^oNA zk-^Cnw~K%zccXdbyH+rCSnH?5OU%s-ph`(@T~mhgMnX&`Rz;)!-9|VVat6$8e{J*q zuz$UXJcQUW%|SD%hg9139V+)1vOpVVRSGc)thO7mjO@&=W-y>0Z>VQs)6E^kRb|ns zm2ko(9Qdkkn>zL=}_DvObOWT|$ho_t_gomTZ>wsc` zO5w0*Wc-uo<>Jx|_jy}58K!^&kKbR_nrO=_urxOgN}+ECg$xZVdd?wSk9Wg1ALQbd z2=r6Lzx+WF1J?owODtpO6(v);5Z-w^z)F54tmq*u-vl)jseG;-gMF;vq!*?Dym-&g zeV=r=EEp|Rz;&_euObaQ)Qa}ExE08=HysF36R>CyMcRPx@g4?&kR zzUqDr+bHvz&fxL(42x+qMrKaXOwmuVOhKmjr-Y|=K%>4+F8SxB4t|426wnn1sK44Sf$$cN_w{Nlr#l!)eEolC25Tg~9LX-4sy#x;K zZ^`=~o{@(j$j+D^{WPf$Dft>HW@=BdQQ}%>*1>$1$2KgheEt?IUV64>R(h^xP6^+H zSDI;>QHq?dBM(_|BKz{Wt2o9+&`IdSps@b<7L(rNV}sKj$|UHkC3}n7X|m7$F4I zJ8=V!(7C3LmX?Rl6l`xP&Q6d8m_){$k{I1<^r+rVAAEIyoMCPU^`y!0c-F73)hjq{ z^~VGBM(+nMpIU)Qxfp)_tMsglxmWV;snm|*tE@j>{z`FzCrmHoH5Px>^!F^*AnNRy zR82Fp0@y+78=TFzJNlemJa@+!g|Q-rQIv4?WpRx3ZpCsK6_nyI$ zk9PJ^o8GeOXwGYuj&~RKx9)soxUSMh&qlVMcJ5*m?{?DgYL+%&TH?g^HQ!) zQt7tbX_9y{QmvM?^|(L-S95&?4cF8GT-LaOsL&YrR!^di+2e(gW!IK@8Ef1m3<$r*9Fiv&5iA9iLZ zc*_4Kfa@7jA3&f(1)CwP5N!rIhv+qhU*E4%0izjYFMC*lZ1RvcY^os(A~9q^APaiuxh5GNPIlu8*!iEI+-16GM_faCvgmqCDU>lwEmt3yb_s&no1Mrj&AZQF+S8ZSKdFHQO0V$e4|N?Ilt zGPPeYv-sG-24~ZnD1xj?YC=8@L7IAU#63}>_0CcIkq9^$I>a-HQwc1crxBc{%JG#N z7L#-dgFnn6?X{%;Pf*na<^{Lj==(&xX>coYXV1imjZ%?I(Eto57jO z7dlgL;Aph&QVUC)oED;8**vDo<&_m(%DjjIA^-+PVC2gSXcEXa^ND~<0~1h01oj8x zLjH3H|7n;0W3~Qi$^VBJ?LT%NbWi_l9nk+zF8e7PP)8&D1*@d%^~vV)-ilW0J%FOx zCAf7PJvBnt`9n7ETFfD)SI0`Xd@@wUG&mn-Zx!J}n~w zJNH0~4^cMb03FUc*2rERJ;BbEKTDy2{mcgXj|I?U-&Ym^-RHh_5w5yU^xq8rnW?r# tVQ=J4>VNAkV?lm8 mListeners = new ArrayList(); - private MyMidiReceiver mReceiver = new MyMidiReceiver(); - private MidiFramer mMidiFramer = new MidiFramer(mReceiver); - private boolean mRecordEnabled = true; - - private static AudioMidiTester mInstance; - private AudioRecordThread mRecorder; - private TapLatencyAnalyser mTapLatencyAnalyser; - - private AudioOutputTester mAudioOutputTester; - - public static class TestResult { - public float[] samples; - public float[] filtered; - public int frameRate; - public TapLatencyAnalyser.TapLatencyEvent[] events; - } - - public static interface TestListener { - public void onTestFinished(TestResult result); - - public void onNoteOn(int pitch); - } - - /** - * This is a Service so it is only created when a client requests the service. - */ - public AudioMidiTester() { - mInstance = this; - } - - public void addTestListener(TestListener listener) { - mListeners.add(listener); - } - - public void removeTestListener(TestListener listener) { - mListeners.remove(listener); - } - - @Override - public void onCreate() { - super.onCreate(); - if (mRecordEnabled) { - mRecorder = new AudioRecordThread(ANALYSIS_SAMPLE_RATE, - 1, - (int) (ANALYSIS_TIME_MAX * ANALYSIS_SAMPLE_RATE)); - } - - mAudioOutputTester = AudioOutputTester.getInstance(); - - mTapLatencyAnalyser = new TapLatencyAnalyser(); - } - - @Override - public void onDestroy() { - // do stuff here - super.onDestroy(); - } - - public static AudioMidiTester getInstance() { - return mInstance; - } - - class MyMidiReceiver extends MidiReceiver { - public void onSend(byte[] data, int offset, - int count, long timestamp) throws IOException { - // parse MIDI - byte command = (byte) (data[0] & 0x0F0); - if (command == MidiConstants.STATUS_NOTE_ON) { - if (data[2] == 0) { - noteOff(data[1]); - } else { - noteOn(data[1]); - } - } else if (command == MidiConstants.STATUS_NOTE_OFF) { - noteOff(data[1]); - } - Log.i(TapToToneActivity.TAG, "MIDI command = " + command); - } - } - - private void noteOn(byte b) { - setEnabled(true); - fireNoteOn(b); - } - - private void fireNoteOn(byte pitch) { - for (TestListener listener : mListeners) { - listener.onNoteOn(pitch); - } - } - - private void noteOff(byte b) { - setEnabled(false); - } - - @Override - public MidiReceiver[] onGetInputPortReceivers() { - return new MidiReceiver[]{mMidiFramer}; - } - - - public void start() throws IOException { - if (mRecordEnabled) { - mRecorder.startAudio(); - } - } - - public void setEnabled(boolean checked) { - mAudioOutputTester.setEnabled(checked); - if (checked && mRecordEnabled) { - // schedule an analysis to start in the near future - int numSamples = (int) (mRecorder.getSampleRate() * ANALYSIS_TIME_DELAY); - Runnable task = new Runnable() { - public void run() { - new Thread() { - public void run() { - analyzeCapturedAudio(); - } - }.start(); - } - }; - - mRecorder.scheduleTask(numSamples, task); - } - } - - private void analyzeCapturedAudio() { - if (!mRecordEnabled) return; - int numSamples = (int) (mRecorder.getSampleRate() * ANALYSIS_TIME_TOTAL); - float[] buffer = new float[numSamples]; - mRecorder.setCaptureEnabled(false); // TODO wait for it to settle - int numRead = mRecorder.readMostRecent(buffer); - - TestResult result = new TestResult(); - result.samples = buffer; - result.frameRate = mRecorder.getSampleRate(); - result.events = mTapLatencyAnalyser.analyze(buffer, 0, numRead); - result.filtered = mTapLatencyAnalyser.getFilteredBuffer(); - mRecorder.setCaptureEnabled(true); - // notify listeners - for (TestListener listener : mListeners) { - listener.onTestFinished(result); - } - } - - - public void stop() { - if (mRecordEnabled) { - mRecorder.stopAudio(); - } - } - -} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AutoGlitchActivity.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AutoGlitchActivity.java deleted file mode 100644 index 62f6f802..00000000 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AutoGlitchActivity.java +++ /dev/null @@ -1,328 +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. - */ - -package com.google.sample.oboe.manualtest; - -import android.content.Intent; -import android.os.Build; -import android.os.Bundle; -import android.text.method.ScrollingMovementMethod; -import android.view.View; -import android.widget.AdapterView; -import android.widget.Spinner; -import android.widget.TextView; - -import java.io.IOException; -import java.util.Date; - -public class AutoGlitchActivity extends GlitchActivity implements Runnable { - - private static final int SETUP_TIME_SECONDS = 4; // Time for the stream to settle. - private static final int DEFAULT_DURATION_SECONDS = 8; // Run time for each test. - private static final int DEFAULT_GAP_MILLIS = 400; // Run time for each test. - private static final String TEXT_SKIP = "SKIP"; - public static final String TEXT_PASS = "PASS"; - public static final String TEXT_FAIL = "FAIL !!!!"; - - private TextView mAutoTextView; - - private Thread mAutoThread; - private volatile boolean mThreadEnabled = false; - int mTestCount = 0; - private int mDurationSeconds = DEFAULT_DURATION_SECONDS; - private int mGapMillis = DEFAULT_GAP_MILLIS; - private StringBuffer mFailedSummary; - private int mPassCount = 0; - private int mFailCount = 0; - private Spinner mDurationSpinner; - - // Test with these configurations. - private static final int[] PERFORMANCE_MODES = { - StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY, - StreamConfiguration.PERFORMANCE_MODE_NONE - }; - private static final int[] SAMPLE_RATES = { 48000, 44100, 16000 }; - - private class DurationSpinnerListener implements android.widget.AdapterView.OnItemSelectedListener { - @Override - public void onItemSelected(AdapterView parent, View view, int pos, long id) { - String text = parent.getItemAtPosition(pos).toString(); - mDurationSeconds = Integer.parseInt(text); - } - - @Override - public void onNothingSelected(AdapterView parent) { - mDurationSeconds = DEFAULT_DURATION_SECONDS; - } - } - - @Override - protected void inflateActivity() { - setContentView(R.layout.activity_auto_glitches); - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - mAutoTextView = (TextView) findViewById(R.id.text_log); - mAutoTextView.setMovementMethod(new ScrollingMovementMethod()); - - mDurationSpinner = (Spinner) findViewById(R.id.spinner_glitch_duration); - mDurationSpinner.setOnItemSelectedListener(new DurationSpinnerListener()); - } - - // Write to scrollable TextView - private void log(final String text) { - runOnUiThread(new Runnable() { - @Override - public void run() { - mAutoTextView.append(text); - mAutoTextView.append("\n"); - } - }); - } - private void logClear() { - runOnUiThread(new Runnable() { - @Override - public void run() { - mAutoTextView.setText(""); - } - }); - } - - public void startAudioTest() { - mThreadEnabled = true; - mAutoThread = new Thread(this); - mAutoThread.start(); - } - - // Only call from UI thread. - @Override - public void onTestFinished() { - super.onTestFinished(); - } - - public void stopAudioTest() { - try { - if (mAutoThread != null) { - mThreadEnabled = false; - mAutoThread.interrupt(); - mAutoThread.join(100); - mAutoThread = null; - } - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - - // Share text from log via GMail, Drive or other method. - public void onShareResult(View view) { - Intent sharingIntent = new Intent(android.content.Intent.ACTION_SEND); - sharingIntent.setType("text/plain"); - - String subjectText = "OboeTester AutoGlitch result " + getTimestampString(); - sharingIntent.putExtra(android.content.Intent.EXTRA_SUBJECT, subjectText); - - String shareBody = mAutoTextView.getText().toString(); - sharingIntent.putExtra(android.content.Intent.EXTRA_TEXT, shareBody); - - startActivity(Intent.createChooser(sharingIntent, "Share using:")); - } - - private String getConfigText(StreamConfiguration config) { - return ((config.getDirection() == StreamConfiguration.DIRECTION_OUTPUT) ? "OUT" : "IN") - + ", SR = " + config.getSampleRate() - + ", Perf = " + StreamConfiguration.convertPerformanceModeToText( - config.getPerformanceMode()) - + ", " + StreamConfiguration.convertSharingModeToText(config.getSharingMode()) - + ", ch = " + config.getChannelCount(); - } - - private void testConfiguration(int perfMode, - int sharingMode, - int sampleRate, - int inChannels, - int outChannels) throws InterruptedException { - - // Configure settings - StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration; - StreamConfiguration actualInConfig = mAudioInputTester.actualConfiguration; - StreamConfiguration requestedOutConfig = mAudioOutTester.requestedConfiguration; - StreamConfiguration actualOutConfig = mAudioOutTester.actualConfiguration; - - requestedInConfig.reset(); - requestedOutConfig.reset(); - - requestedInConfig.setPerformanceMode(perfMode); - requestedOutConfig.setPerformanceMode(perfMode); - - requestedInConfig.setSharingMode(sharingMode); - requestedOutConfig.setSharingMode(sharingMode); - - requestedInConfig.setSampleRate(sampleRate); - requestedOutConfig.setSampleRate(sampleRate); - - requestedInConfig.setChannelCount(inChannels); - requestedOutConfig.setChannelCount(outChannels); - - log("========================== #" + mTestCount); - log("Requested:"); - log(getConfigText(requestedInConfig)); - log(getConfigText(requestedOutConfig)); - - // Give previous stream time to close and release resources. Avoid race conditions. - Thread.sleep(1000); - boolean openFailed = false; - try { - super.startAudioTest(); // this will fill in actualConfig - log("Actual:"); - log(getConfigText(actualInConfig)); - log(getConfigText(actualOutConfig)); - // Set output size to a level that will avoid glitches. - AudioStreamBase stream = mAudioOutTester.getCurrentAudioStream(); - int sizeFrames = stream.getBufferCapacityInFrames() / 2; - stream.setBufferSizeInFrames(sizeFrames); - } catch (IOException e) { - openFailed = true; - log(e.getMessage()); - } - - // The test would only be worth running if we got the configuration we requested on input or output. - boolean valid = true; - // No point running the test if we don't get the sharing mode we requested. - if (!openFailed && actualInConfig.getSharingMode() != sharingMode - && actualOutConfig.getSharingMode() != sharingMode) { - log("did not get requested sharing mode"); - valid = false; - } - // We don't skip based on performance mode because if you request LOW_LATENCY you might - // get a smaller burst than if you request NONE. - - if (!openFailed && valid) { - Thread.sleep(mDurationSeconds * 1000); - } - int inXRuns = 0; - int outXRuns = 0; - - if (!openFailed) { - // get xRuns before closing the streams. - inXRuns = mAudioInputTester.getCurrentAudioStream().getXRunCount(); - outXRuns = mAudioOutTester.getCurrentAudioStream().getXRunCount(); - - super.stopAudioTest(); - } - - if (valid) { - if (openFailed) { - mFailedSummary.append("------ #" + mTestCount); - mFailedSummary.append("\n"); - mFailedSummary.append(getConfigText(requestedInConfig)); - mFailedSummary.append("\n"); - mFailedSummary.append(getConfigText(requestedOutConfig)); - mFailedSummary.append("\n"); - mFailedSummary.append("Open failed!\n"); - mFailCount++; - } else { - log("Result:"); - boolean passed = (getMaxSecondsWithNoGlitch() - > (mDurationSeconds - SETUP_TIME_SECONDS)); - String resultText = getShortReport(); - resultText += ", xruns = " + inXRuns + "/" + outXRuns; - resultText += ", " + (passed ? TEXT_PASS : TEXT_FAIL); - log(resultText); - if (!passed) { - mFailedSummary.append("------ #" + mTestCount); - mFailedSummary.append("\n"); - mFailedSummary.append(" "); - mFailedSummary.append(getConfigText(actualInConfig)); - mFailedSummary.append("\n"); - mFailedSummary.append(" "); - mFailedSummary.append(resultText); - mFailedSummary.append("\n"); - mFailCount++; - } else { - mPassCount++; - } - } - } else { - log(TEXT_SKIP); - } - // Give hardware time to settle between tests. - Thread.sleep(mGapMillis); - mTestCount++; - } - - private void testConfiguration(int performanceMode, - int sharingMode, - int sampleRate) throws InterruptedException { - testConfiguration(performanceMode, - sharingMode, - sampleRate, 1, 2); - testConfiguration(performanceMode, - sharingMode, - sampleRate, 2, 1); - } - - - @Override - public void run() { - logClear(); - log("=== STARTED at " + new Date()); - log(Build.MANUFACTURER + " " + Build.PRODUCT); - log(Build.DISPLAY); - mFailedSummary = new StringBuffer(); - mTestCount = 0; - mPassCount = 0; - mFailCount = 0; - try { - testConfiguration(StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY, - StreamConfiguration.SHARING_MODE_EXCLUSIVE, - 0); - testConfiguration(StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY, - StreamConfiguration.SHARING_MODE_SHARED, - 0); - - for (int perfMode : PERFORMANCE_MODES) { - for (int sampleRate : SAMPLE_RATES) { - testConfiguration(perfMode, - StreamConfiguration.SHARING_MODE_SHARED, - sampleRate); - } - } - } catch (InterruptedException e) { - e.printStackTrace(); - } finally { - super.stopAudioTest(); - log("\n==== SUMMARY ========"); - if (mFailCount > 0) { - log(mPassCount + " passed. " + mFailCount + " failed."); - log("These tests FAILED:"); - log(mFailedSummary.toString()); - } else { - log("All tests PASSED."); - } - log("== FINISHED at " + new Date()); - runOnUiThread(new Runnable() { - @Override - public void run() { - onTestFinished(); - } - }); - } - } - -} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/BufferSizeView.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/BufferSizeView.java deleted file mode 100644 index 04ce9413..00000000 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/BufferSizeView.java +++ /dev/null @@ -1,143 +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. - */ - -package com.google.sample.oboe.manualtest; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.LayoutInflater; - - -import android.widget.SeekBar; -import android.widget.TextView; -import android.widget.LinearLayout; - -public class BufferSizeView extends LinearLayout { - - AudioOutputTester mAudioOutTester; - - protected static final int FADER_THRESHOLD_MAX = 1000; // must match layout - protected TextView mTextThreshold; - protected SeekBar mFaderThreshold; - protected ExponentialTaper mTaperThreshold; - private int mCachedCapacity; - - private SeekBar.OnSeekBarChangeListener mThresholdListener = new SeekBar.OnSeekBarChangeListener() { - @Override - public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - setBufferSizeByPosition(progress); - } - - @Override - public void onStartTrackingTouch(SeekBar seekBar) { - } - - @Override - public void onStopTrackingTouch(SeekBar seekBar) { - } - }; - - public BufferSizeView(Context context) { - super(context); - initializeViews(context); - } - - public BufferSizeView(Context context, AttributeSet attrs) { - super(context, attrs); - initializeViews(context); - } - - public BufferSizeView(Context context, - AttributeSet attrs, - int defStyle) { - super(context, attrs, defStyle); - initializeViews(context); - } - - public AudioOutputTester getAudioOutTester() { - return mAudioOutTester; - } - - public void setAudioOutTester(AudioOutputTester audioOutTester) { - mAudioOutTester = audioOutTester; - } - - void setFaderNormalizedProgress(double fraction) { - mFaderThreshold.setProgress((int)(fraction * FADER_THRESHOLD_MAX)); - } - - /** - * Inflates the views in the layout. - * - * @param context - * the current context for the view. - */ - private void initializeViews(Context context) { - LayoutInflater inflater = (LayoutInflater) context - .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - inflater.inflate(R.layout.buffer_size_view, this); - - mTextThreshold = (TextView) findViewById(R.id.textThreshold); - mFaderThreshold = (SeekBar) findViewById(R.id.faderThreshold); - mFaderThreshold.setOnSeekBarChangeListener(mThresholdListener); - mTaperThreshold = new ExponentialTaper(0.0, 1.0, 10.0); - mFaderThreshold.setProgress(0); - } - - private void setBufferSizeByPosition(int progress) { - StringBuffer message = new StringBuffer(); - double normalizedThreshold = mTaperThreshold.linearToExponential( - ((double)progress)/FADER_THRESHOLD_MAX); - if (normalizedThreshold < 0.0) normalizedThreshold = 0.0; - else if (normalizedThreshold > 1.0) normalizedThreshold = 1.0; - int percent = (int) (normalizedThreshold * 100); - message.append("bufferSize = " + percent + "%"); - - OboeAudioStream stream = null; - int sizeFrames = 0; - if (getAudioOutTester() != null) { - stream = (OboeAudioStream) getAudioOutTester().getCurrentAudioStream(); - if (stream != null) { - int capacity = stream.getBufferCapacityInFrames(); - if (capacity > 0) mCachedCapacity = capacity; - } - } - if (mCachedCapacity > 0) { - sizeFrames = (int) (normalizedThreshold * mCachedCapacity); - message.append(" = " + sizeFrames); - if (stream != null) { - stream.setBufferSizeInFrames(sizeFrames); - } - int bufferSize = getAudioOutTester().getCurrentAudioStream().getBufferSizeInFrames(); - if (bufferSize >= 0) { - message.append(" / " + bufferSize); - } - message.append(" / " + mCachedCapacity); - } - mTextThreshold.setText(message.toString()); - } - - public void updateBufferSize() { - int progress = mFaderThreshold.getProgress(); - setBufferSizeByPosition(progress); - } - - @Override - public void setEnabled(boolean enabled) { - super.setEnabled(enabled); - mFaderThreshold.setEnabled(enabled); - } -} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/ManualGlitchActivity.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/ManualGlitchActivity.java deleted file mode 100644 index 3cdb6f56..00000000 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/ManualGlitchActivity.java +++ /dev/null @@ -1,286 +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. - */ - -package com.google.sample.oboe.manualtest; - -import android.content.Intent; -import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; -import android.widget.SeekBar; -import android.widget.TextView; - -import java.io.IOException; -import java.util.stream.Stream; - -public class ManualGlitchActivity extends GlitchActivity { - - public static final String KEY_IN_PERF = "in_perf"; - public static final String KEY_OUT_PERF = "out_perf"; - public static final String VALUE_PERF_LOW_LATENCY = "lowlat"; - public static final String VALUE_PERF_POWERSAVE = "powersave"; - public static final String VALUE_PERF_NONE = "none"; - - public static final String KEY_IN_SHARING = "in_sharing"; - public static final String KEY_OUT_SHARING = "out_sharing"; - public static final String VALUE_SHARING_EXCLUSIVE = "exclusive"; - public static final String VALUE_SHARING_SHARED = "shared"; - - public static final String KEY_SAMPLE_RATE = "sample_rate"; - public static final int VALUE_DEFAULT_SAMPLE_RATE = 48000; - - public static final String KEY_IN_PRESET = "in_preset"; - - public static final String KEY_IN_CHANNELS = "in_channels"; - public static final String KEY_OUT_CHANNELS = "out_channels"; - public static final int VALUE_DEFAULT_CHANNELS = 2; - - public static final String KEY_DURATION = "duration"; - public static final int VALUE_DEFAULT_DURATION = 10; - - public static final String KEY_BUFFER_BURSTS = "buffer_bursts"; - public static final int VALUE_DEFAULT_BUFFER_BURSTS = 2; - - public static final String KEY_TOLERANCE = "tolerance"; - private static final float DEFAULT_TOLERANCE = 0.1f; - - private TextView mTextTolerance; - private SeekBar mFaderTolerance; - protected ExponentialTaper mTaperTolerance; - private WaveformView mWaveformView; - private float[] mWaveform = new float[256]; - private boolean mTestRunningByIntent; - private Bundle mBundleFromIntent; - - private float mTolerance = DEFAULT_TOLERANCE; - - private SeekBar.OnSeekBarChangeListener mToleranceListener = new SeekBar.OnSeekBarChangeListener() { - @Override - public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - setToleranceProgress(progress); - } - - @Override - public void onStartTrackingTouch(SeekBar seekBar) { - } - - @Override - public void onStopTrackingTouch(SeekBar seekBar) { - } - }; - - protected void setToleranceProgress(int progress) { - float tolerance = (float) mTaperTolerance.linearToExponential( - ((double)progress) / FADER_PROGRESS_MAX); - setTolerance(tolerance); - mTextTolerance.setText("Tolerance = " + String.format("%5.3f", tolerance)); - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - mBundleFromIntent = getIntent().getExtras(); - - mTextTolerance = (TextView) findViewById(R.id.textTolerance); - mFaderTolerance = (SeekBar) findViewById(R.id.faderTolerance); - mTaperTolerance = new ExponentialTaper(0.0, 0.5, 100.0); - mFaderTolerance.setOnSeekBarChangeListener(mToleranceListener); - setToleranceFader(DEFAULT_TOLERANCE); - - mWaveformView = (WaveformView) findViewById(R.id.waveview_audio); - } - - private void setToleranceFader(float tolerance) { - int progress = (int) Math.round((mTaperTolerance.exponentialToLinear( - tolerance) * FADER_PROGRESS_MAX)); - mFaderTolerance.setProgress(progress); - } - - @Override - protected void inflateActivity() { - setContentView(R.layout.activity_manual_glitches); - } - - @Override - public void onResume(){ - super.onResume(); - processBundleFromIntent(); - } - - @Override - public void onNewIntent(Intent intent) { - mBundleFromIntent = intent.getExtras(); - } - - private void processBundleFromIntent() { - if (mBundleFromIntent == null) { - return; - } - if (mTestRunningByIntent) { - return; - } - - mResultFileName = null; - if (mBundleFromIntent.containsKey(KEY_FILE_NAME)) { - mTestRunningByIntent = true; - mResultFileName = mBundleFromIntent.getString(KEY_FILE_NAME); - - // Delay the test start to avoid race conditions. - Handler handler = new Handler(Looper.getMainLooper()); // UI thread - handler.postDelayed(new Runnable() { - @Override - public void run() { - startAutomaticTest(); - } - }, 500); // TODO where is the race, close->open? - - } - } - - private int getPerfFromText(String text) { - if (VALUE_PERF_NONE.equals(text)) { - return StreamConfiguration.PERFORMANCE_MODE_NONE; - } else if (VALUE_PERF_POWERSAVE.equals(text)) { - return StreamConfiguration.PERFORMANCE_MODE_POWER_SAVING; - } else { - return StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY; - } - } - - private int getSharingFromText(String text) { - if (VALUE_SHARING_SHARED.equals(text)) { - return StreamConfiguration.SHARING_MODE_SHARED; - } else { - return StreamConfiguration.SHARING_MODE_EXCLUSIVE; - } - } - - void configureStreamsFromBundle(Bundle bundle) { - - // Configure settings - StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration; - StreamConfiguration requestedOutConfig = mAudioOutTester.requestedConfiguration; - - requestedInConfig.reset(); - requestedOutConfig.reset(); - - // Extract parameters from the bundle. - String text = bundle.getString(KEY_IN_PERF, VALUE_PERF_LOW_LATENCY); - int perfMode = getPerfFromText(text); - requestedInConfig.setPerformanceMode(perfMode); - - text = bundle.getString(KEY_OUT_PERF, VALUE_PERF_LOW_LATENCY); - perfMode = getPerfFromText(text); - requestedOutConfig.setPerformanceMode(perfMode); - - text = bundle.getString(KEY_IN_SHARING, VALUE_SHARING_EXCLUSIVE); - int sharingMode = getSharingFromText(text); - requestedInConfig.setSharingMode(sharingMode); - text = bundle.getString(KEY_OUT_SHARING, VALUE_SHARING_EXCLUSIVE); - sharingMode = getSharingFromText(text); - requestedOutConfig.setSharingMode(sharingMode); - - int sampleRate = bundle.getInt(KEY_SAMPLE_RATE, VALUE_DEFAULT_SAMPLE_RATE); - requestedInConfig.setSampleRate(sampleRate); - requestedOutConfig.setSampleRate(sampleRate); - - float tolerance = bundle.getFloat(KEY_TOLERANCE, DEFAULT_TOLERANCE); - setToleranceFader(tolerance); - setTolerance(tolerance); - mTolerance = tolerance; - - int inChannels = bundle.getInt(KEY_IN_CHANNELS, VALUE_DEFAULT_CHANNELS); - requestedInConfig.setChannelCount(inChannels); - int outChannels = bundle.getInt(KEY_OUT_CHANNELS, VALUE_DEFAULT_CHANNELS); - requestedOutConfig.setChannelCount(outChannels); - - String defaultText = StreamConfiguration.convertInputPresetToText( - StreamConfiguration.INPUT_PRESET_VOICE_RECOGNITION); - text = bundle.getString(KEY_IN_PRESET, defaultText); - int inputPreset = StreamConfiguration.convertTextToInputPreset(text); - requestedInConfig.setInputPreset(inputPreset); - } - - public void startAudioTest() throws IOException { - super.startAudioTest(); - setToleranceProgress(mFaderTolerance.getProgress()); - } - - void startAutomaticTest() { - configureStreamsFromBundle(mBundleFromIntent); - - int durationSeconds = mBundleFromIntent.getInt(KEY_DURATION, VALUE_DEFAULT_DURATION); - int numBursts = mBundleFromIntent.getInt(KEY_BUFFER_BURSTS, VALUE_DEFAULT_BUFFER_BURSTS); - mBundleFromIntent = null; - - try { - onStartAudioTest(null); - int sizeFrames = mAudioOutTester.getCurrentAudioStream().getFramesPerBurst() * numBursts; - mAudioOutTester.getCurrentAudioStream().setBufferSizeInFrames(sizeFrames); - - // Schedule the end of the test. - Handler handler = new Handler(Looper.getMainLooper()); // UI thread - handler.postDelayed(new Runnable() { - @Override - public void run() { - stopAutomaticTest(); - } - }, durationSeconds * 1000); - } catch (IOException e) { - String report = "Open failed: " + e.getMessage(); - maybeWriteTestResult(report); - mTestRunningByIntent = false; - } - - } - - void stopAutomaticTest() { - String report = getCommonTestReport() - + String.format("tolerance = %5.3f\n", mTolerance) - + mLastGlitchReport; - onStopAudioTest(null); - maybeWriteTestResult(report); - mTestRunningByIntent = false; - } - - // Only call from UI thread. - @Override - public void onTestFinished() { - super.onTestFinished(); - } - // Only call from UI thread. - @Override - public void onTestBegan() { - mWaveformView.clearSampleData(); - mWaveformView.postInvalidate(); - super.onTestBegan(); - } - - // Called on UI thread - @Override - protected void onGlitchDetected() { - int numSamples = getGlitch(mWaveform); - mWaveformView.setSampleData(mWaveform, 0, numSamples); - mWaveformView.postInvalidate(); - } - - private float[] getGlitchWaveform() { - return mWaveform; - } - - private native int getGlitch(float[] mWaveform); - -} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/NativeEngine.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/NativeEngine.java deleted file mode 100644 index 9917c662..00000000 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/NativeEngine.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.google.sample.oboe.manualtest; - -public class NativeEngine { - - static native boolean isMMapSupported(); - - static native boolean isMMapExclusiveSupported(); - - static native void setWorkaroundsEnabled(boolean enabled); -} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/RoundTripLatencyActivity.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/RoundTripLatencyActivity.java deleted file mode 100644 index 782950a7..00000000 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/RoundTripLatencyActivity.java +++ /dev/null @@ -1,395 +0,0 @@ -/* - * Copyright 2018 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. - */ - -package com.google.sample.oboe.manualtest; - -import android.content.Intent; -import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; -import android.support.annotation.NonNull; -import android.view.View; -import android.widget.Button; -import android.widget.TextView; - -import java.io.IOException; - -/** - * Activity to measure latency on a full duplex stream. - */ -public class RoundTripLatencyActivity extends AnalyzerActivity { - - private static final int STATE_GOT_DATA = 2; // Defined in LatencyAnalyzer.h - private final static String LATENCY_FORMAT = "%4.2f"; - private final static String CONFIDENCE_FORMAT = "%5.3f"; - - private TextView mAnalyzerView; - private Button mMeasureButton; - private Button mAverageButton; - private Button mCancelButton; - private Button mShareButton; - private boolean mHasRecording = false; - - private boolean mTestRunningByIntent; - private Bundle mBundleFromIntent; - private int mBufferBursts = -1; - private Handler mHandler = new Handler(Looper.getMainLooper()); // UI thread - - // Run the test several times and report the acverage latency. - protected class LatencyAverager { - private final static int AVERAGE_TEST_DELAY_MSEC = 1000; // arbitrary - private static final int AVERAGE_MAX_ITERATIONS = 10; // arbitrary - private int mCount = 0; - - private double mWeightedLatencySum; - private double mLatencyMin; - private double mLatencyMax; - private double mConfidenceSum; - private boolean mActive; - private String mLastReport = ""; - - // Called on UI thread. - String onAnalyserDone() { - String message; - if (!mActive) { - message = ""; - } else if (getMeasuredResult() != 0) { - cancel(); - updateButtons(false); - message = "averaging cancelled due to error\n"; - } else { - mCount++; - double latency = getMeasuredLatencyMillis(); - double confidence = getMeasuredConfidence(); - mWeightedLatencySum += latency * confidence; // weighted average based on confidence - mConfidenceSum += confidence; - mLatencyMin = Math.min(mLatencyMin, latency); - mLatencyMax = Math.max(mLatencyMax, latency); - if (mCount < AVERAGE_MAX_ITERATIONS) { - mHandler.postDelayed(new Runnable() { - @Override - public void run() { - measureSingleLatency(); - } - }, AVERAGE_TEST_DELAY_MSEC); - } else { - mActive = false; - updateButtons(false); - } - message = reportAverage(); - } - return message; - } - - private String reportAverage() { - String message; - if (mCount == 0 || mConfidenceSum == 0.0) { - message = "num.iterations = " + mCount + "\n"; - } else { - // When I use 5.3g I only get one digit after the decimal point! - final double averageLatency = mWeightedLatencySum / mConfidenceSum; - final double mAverageConfidence = mConfidenceSum / mCount; - message = - "average.latency.msec = " + String.format(LATENCY_FORMAT, averageLatency) + "\n" - + "average.confidence = " + String.format(CONFIDENCE_FORMAT, mAverageConfidence) + "\n" - + "min.latency.msec = " + String.format(LATENCY_FORMAT, mLatencyMin) + "\n" - + "max.latency.msec = " + String.format(LATENCY_FORMAT, mLatencyMax) + "\n" - + "num.iterations = " + mCount + "\n"; - } - mLastReport = message; - return message; - } - - // Called on UI thread. - public void start() { - mWeightedLatencySum = 0.0; - mConfidenceSum = 0.0; - mLatencyMax = Double.MIN_VALUE; - mLatencyMin = Double.MAX_VALUE; - mCount = 0; - mActive = true; - mLastReport = ""; - measureSingleLatency(); - } - - public void clear() { - mActive = false; - mLastReport = ""; - } - - public void cancel() { - mActive = false; - } - - public boolean isActive() { - return mActive; - } - - public String getLastReport() { - return mLastReport; - } - } - LatencyAverager mLatencyAverager = new LatencyAverager(); - - // Periodically query the status of the stream. - protected class LatencySniffer { - private int counter = 0; - public static final int SNIFFER_UPDATE_PERIOD_MSEC = 150; - public static final int SNIFFER_UPDATE_DELAY_MSEC = 300; - - - // Display status info for the stream. - private Runnable runnableCode = new Runnable() { - @Override - public void run() { - String message; - - if (isAnalyzerDone()) { - message = onAnalyzerDone(); - message += mLatencyAverager.onAnalyserDone(); - } else { - message = getProgressText(); - message += "please wait... " + counter + "\n"; - if (getAnalyzerState() == STATE_GOT_DATA) { - message += "ANALYZING\n"; - } - // Repeat this runnable code block again. - mHandler.postDelayed(runnableCode, SNIFFER_UPDATE_PERIOD_MSEC); - } - setAnalyzerText(message); - counter++; - } - }; - - private void startSniffer() { - counter = 0; - // Start the initial runnable task by posting through the handler - mHandler.postDelayed(runnableCode, SNIFFER_UPDATE_DELAY_MSEC); - } - - private void stopSniffer() { - if (mHandler != null) { - mHandler.removeCallbacks(runnableCode); - } - } - } - - private String getProgressText() { - int progress = getAnalyzerProgress(); - int state = getAnalyzerState(); - int resetCount = getResetCount(); - String message = String.format("progress = %d, state = %d, #resets = %d\n", - progress, state, resetCount); - message += mLatencyAverager.getLastReport(); - return message; - } - - private String onAnalyzerDone() { - String message = getResultString(); - if (mTestRunningByIntent) { - String report = getCommonTestReport(); - report += message; - maybeWriteTestResult(report); - } - mTestRunningByIntent = false; - mHasRecording = true; - stopAudioTest(); - return message; - } - - @NonNull - private String getResultString() { - String message = String.format("rms.signal = %7.5f\n", getSignalRMS()); - message += String.format("rms.noise = %7.5f\n", getBackgroundRMS()); - int resetCount = getResetCount(); - message += String.format("reset.count = %d\n", resetCount); - - int result = getMeasuredResult(); - message += String.format("result = %d\n", result); - message += String.format("result.text = %s\n", resultCodeToString(result)); - - // Only report valid latencies. - if (result == 0) { - int latencyFrames = getMeasuredLatency(); - double latencyMillis = getMeasuredLatencyMillis(); - int bufferSize = mAudioOutTester.getCurrentAudioStream().getBufferSizeInFrames(); - int latencyEmptyFrames = latencyFrames - bufferSize; - double latencyEmptyMillis = latencyEmptyFrames * 1000.0 / getSampleRate(); - message += String.format("latency.empty.frames = %d\n", latencyEmptyFrames); - message += String.format("latency.empty.msec = " + LATENCY_FORMAT + "\n", latencyEmptyMillis); - message += String.format("latency.frames = %d\n", latencyFrames); - message += String.format("latency.msec = " + LATENCY_FORMAT + "\n", latencyMillis); - } - double confidence = getMeasuredConfidence(); - message += String.format("confidence = " + CONFIDENCE_FORMAT + "\n", confidence); - return message; - } - - private LatencySniffer mLatencySniffer = new LatencySniffer(); - - native int getAnalyzerProgress(); - native int getMeasuredLatency(); - double getMeasuredLatencyMillis() { - return getMeasuredLatency() * 1000.0 / getSampleRate(); - } - native double getMeasuredConfidence(); - native double getBackgroundRMS(); - native double getSignalRMS(); - - private void setAnalyzerText(String s) { - mAnalyzerView.setText(s); - } - - @Override - protected void inflateActivity() { - setContentView(R.layout.activity_rt_latency); - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - mMeasureButton = (Button) findViewById(R.id.button_measure); - mAverageButton = (Button) findViewById(R.id.button_average); - mCancelButton = (Button) findViewById(R.id.button_cancel); - mShareButton = (Button) findViewById(R.id.button_share); - mShareButton.setEnabled(false); - mAnalyzerView = (TextView) findViewById(R.id.text_status); - updateEnabledWidgets(); - - hideSettingsViews(); - - mBufferSizeView.setFaderNormalizedProgress(0.0); // for lowest latency - - mBundleFromIntent = getIntent().getExtras(); - } - - @Override - public void onNewIntent(Intent intent) { - mBundleFromIntent = intent.getExtras(); - } - - @Override - protected void onStart() { - super.onStart(); - setActivityType(ACTIVITY_RT_LATENCY); - mHasRecording = false; - updateButtons(false); - } - - private void processBundleFromIntent() { - if (mBundleFromIntent == null) { - return; - } - if (mTestRunningByIntent) { - return; - } - - mResultFileName = null; - if (mBundleFromIntent.containsKey(KEY_FILE_NAME)) { - mTestRunningByIntent = true; - mResultFileName = mBundleFromIntent.getString(KEY_FILE_NAME); - getFirstInputStreamContext().configurationView.setExclusiveMode(true); - getFirstOutputStreamContext().configurationView.setExclusiveMode(true); - mBufferBursts = mBundleFromIntent.getInt(KEY_BUFFER_BURSTS, mBufferBursts); - - // Delay the test start to avoid race conditions. - Handler handler = new Handler(Looper.getMainLooper()); // UI thread - handler.postDelayed(new Runnable() { - @Override - public void run() { - onMeasure(null); - } - }, 500); // TODO where is the race, close->open? - - } - mBundleFromIntent = null; - } - - @Override - public void onResume(){ - super.onResume(); - processBundleFromIntent(); - } - - @Override - protected void onStop() { - mLatencySniffer.stopSniffer(); - super.onStop(); - } - - public void onMeasure(View view) { - mLatencyAverager.clear(); - measureSingleLatency(); - } - - void updateButtons(boolean running) { - boolean busy = running || mLatencyAverager.isActive(); - mMeasureButton.setEnabled(!busy); - mAverageButton.setEnabled(!busy); - mCancelButton.setEnabled(running); - mShareButton.setEnabled(!busy && mHasRecording); - } - - private void measureSingleLatency() { - try { - openAudio(); - if (mBufferBursts >= 0) { - AudioStreamBase stream = mAudioOutTester.getCurrentAudioStream(); - int framesPerBurst = stream.getFramesPerBurst(); - stream.setBufferSizeInFrames(framesPerBurst * mBufferBursts); - // override buffer size fader - mBufferSizeView.setEnabled(false); - mBufferBursts = -1; - } - startAudio(); - mLatencySniffer.startSniffer(); - updateButtons(true); - } catch (IOException e) { - showErrorToast(e.getMessage()); - } - } - - public void onAverage(View view) { - mLatencyAverager.start(); - } - - public void onCancel(View view) { - mLatencyAverager.cancel(); - stopAudioTest(); - } - - // Call on UI thread - public void stopAudioTest() { - mLatencySniffer.stopSniffer(); - stopAudio(); - closeAudio(); - updateButtons(false); - } - - @Override - String getWaveTag() { - return "rtlatency"; - } - - @Override - boolean isOutput() { - return false; - } - - @Override - public void setupEffects(int sessionId) { - } -} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/StreamConfiguration.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/StreamConfiguration.java deleted file mode 100644 index e521d84a..00000000 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/StreamConfiguration.java +++ /dev/null @@ -1,355 +0,0 @@ -/* - * Copyright 2017 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. - */ - -package com.google.sample.oboe.manualtest; - -/** - * Container for the properties of a Stream. - * - * This can be used to build a stream, or as a base class for a Stream, - * or as a way to report the properties of a Stream. - */ - -public class StreamConfiguration { - public static final int UNSPECIFIED = 0; - - // These must match order in Spinner and in native code and in AAudio.h - public static final int NATIVE_API_UNSPECIFIED = 0; - public static final int NATIVE_API_OPENSLES = 1; - public static final int NATIVE_API_AAUDIO = 2; - - public static final int SHARING_MODE_EXCLUSIVE = 0; // must match AAUDIO - public static final int SHARING_MODE_SHARED = 1; // must match AAUDIO - - public static final int AUDIO_FORMAT_PCM_16 = 1; // must match AAUDIO - public static final int AUDIO_FORMAT_PCM_FLOAT = 2; // must match AAUDIO - - public static final int DIRECTION_OUTPUT = 0; // must match AAUDIO - public static final int DIRECTION_INPUT = 1; // must match AAUDIO - - public static final int SESSION_ID_NONE = -1; // must match AAUDIO - public static final int SESSION_ID_ALLOCATE = 0; // must match AAUDIO - - public static final int PERFORMANCE_MODE_NONE = 10; // must match AAUDIO - public static final int PERFORMANCE_MODE_POWER_SAVING = 11; // must match AAUDIO - public static final int PERFORMANCE_MODE_LOW_LATENCY = 12; // must match AAUDIO - - public static final int RATE_CONVERSION_QUALITY_NONE = 0; // must match Oboe - public static final int RATE_CONVERSION_QUALITY_FASTEST = 1; // must match Oboe - public static final int RATE_CONVERSION_QUALITY_LOW = 2; // must match Oboe - public static final int RATE_CONVERSION_QUALITY_MEDIUM = 3; // must match Oboe - public static final int RATE_CONVERSION_QUALITY_HIGH = 4; // must match Oboe - public static final int RATE_CONVERSION_QUALITY_BEST = 5; // must match Oboe - - public static final int STREAM_STATE_STARTING = 3; // must match Oboe - public static final int STREAM_STATE_STARTED = 4; // must match Oboe - - public static final int INPUT_PRESET_GENERIC = 1; // must match Oboe - public static final int INPUT_PRESET_CAMCORDER = 5; // must match Oboe - public static final int INPUT_PRESET_VOICE_RECOGNITION = 6; // must match Oboe - public static final int INPUT_PRESET_VOICE_COMMUNICATION = 7; // must match Oboe - public static final int INPUT_PRESET_UNPROCESSED = 9; // must match Oboe - public static final int INPUT_PRESET_VOICE_PERFORMANCE = 10; // must match Oboe - - private int mNativeApi; - private int mBufferCapacityInFrames; - private int mChannelCount; - private int mDeviceId; - private int mSessionId; - private int mDirection; // does not get reset - private int mFormat; - private int mSampleRate; - private int mSharingMode; - private int mPerformanceMode; - private boolean mFormatConversionAllowed; - private boolean mChannelConversionAllowed; - private int mRateConversionQuality; - private int mInputPreset; - - private int mFramesPerBurst = 0; - - private boolean mMMap = false; - - public StreamConfiguration() { - reset(); - } - - public void reset() { - mNativeApi = NATIVE_API_UNSPECIFIED; - mBufferCapacityInFrames = UNSPECIFIED; - mChannelCount = UNSPECIFIED; - mDeviceId = UNSPECIFIED; - mSessionId = -1; - mFormat = AUDIO_FORMAT_PCM_FLOAT; - mSampleRate = UNSPECIFIED; - mSharingMode = SHARING_MODE_EXCLUSIVE; - mPerformanceMode = PERFORMANCE_MODE_LOW_LATENCY; - mInputPreset = INPUT_PRESET_VOICE_RECOGNITION; - mFormatConversionAllowed = false; - mChannelConversionAllowed = false; - mRateConversionQuality = RATE_CONVERSION_QUALITY_NONE; - mMMap = NativeEngine.isMMapSupported(); - } - - public int getFramesPerBurst() { - return mFramesPerBurst; - } - - public void setFramesPerBurst(int framesPerBurst) { - this.mFramesPerBurst = framesPerBurst; - } - - public int getBufferCapacityInFrames() { - return mBufferCapacityInFrames; - } - - public void setBufferCapacityInFrames(int bufferCapacityInFrames) { - this.mBufferCapacityInFrames = bufferCapacityInFrames; - } - - public int getFormat() { - return mFormat; - } - - public void setFormat(int format) { - this.mFormat = format; - } - - public int getDirection() { - return mDirection; - } - - public void setDirection(int direction) { - this.mDirection = direction; - } - - public int getPerformanceMode() { - return mPerformanceMode; - } - - public void setPerformanceMode(int performanceMode) { - this.mPerformanceMode = performanceMode; - } - - public int getInputPreset() { - return mInputPreset; - } - public void setInputPreset(int inputPreset) { - this.mInputPreset = inputPreset; - } - - static String convertPerformanceModeToText(int performanceMode) { - switch(performanceMode) { - case PERFORMANCE_MODE_NONE: - return "NONE"; - case PERFORMANCE_MODE_POWER_SAVING: - return "PWRSAV"; - case PERFORMANCE_MODE_LOW_LATENCY: - return "LOWLAT"; - default: - return "INVALID"; - } - } - - public int getSharingMode() { - return mSharingMode; - } - - public void setSharingMode(int sharingMode) { - this.mSharingMode = sharingMode; - } - - static String convertSharingModeToText(int sharingMode) { - switch(sharingMode) { - case SHARING_MODE_SHARED: - return "SHARED"; - case SHARING_MODE_EXCLUSIVE: - return "EXCLUSIVE"; - default: - return "INVALID"; - } - } - - public static String convertFormatToText(int format) { - switch(format) { - case UNSPECIFIED: - return "Unspecified"; - case AUDIO_FORMAT_PCM_16: - return "I16"; - case AUDIO_FORMAT_PCM_FLOAT: - return "Float"; - default: - return "Invalid"; - } - } - - public static String convertNativeApiToText(int api) { - switch(api) { - case NATIVE_API_UNSPECIFIED: - return "Unspec"; - case NATIVE_API_AAUDIO: - return "AAudio"; - case NATIVE_API_OPENSLES: - return "OpenSL"; - default: - return "Invalid"; - } - } - - - public String dump() { - String prefix = (getDirection() == DIRECTION_INPUT) ? "in" : "out"; - StringBuffer message = new StringBuffer(); - message.append(String.format("%s.channels = %d\n", prefix, mChannelCount)); - message.append(String.format("%s.perf = %s\n", prefix, - convertPerformanceModeToText(mPerformanceMode).toLowerCase())); - if (getDirection() == DIRECTION_INPUT) { - message.append(String.format("%s.preset = %s\n", prefix, - convertInputPresetToText(mInputPreset).toLowerCase())); - } - message.append(String.format("%s.sharing = %s\n", prefix, - convertSharingModeToText(mSharingMode).toLowerCase())); - message.append(String.format("%s.api = %s\n", prefix, - convertNativeApiToText(getNativeApi()).toLowerCase())); - message.append(String.format("%s.rate = %d\n", prefix, mSampleRate)); - message.append(String.format("%s.device = %d\n", prefix, mDeviceId)); - message.append(String.format("%s.mmap = %s\n", prefix, isMMap() ? "yes" : "no")); - message.append(String.format("%s.rate.conversion.quality = %d\n", prefix, mRateConversionQuality)); - return message.toString(); - } - - // text must match menu values - public static final String NAME_INPUT_PRESET_GENERIC = "Generic"; - public static final String NAME_INPUT_PRESET_CAMCORDER = "Camcorder"; - public static final String NAME_INPUT_PRESET_VOICE_RECOGNITION = "VoiceRec"; - public static final String NAME_INPUT_PRESET_VOICE_COMMUNICATION = "VoiceComm"; - public static final String NAME_INPUT_PRESET_UNPROCESSED = "Unprocessed"; - public static final String NAME_INPUT_PRESET_VOICE_PERFORMANCE = "Performance"; - - public static String convertInputPresetToText(int inputPreset) { - switch(inputPreset) { - case INPUT_PRESET_GENERIC: - return NAME_INPUT_PRESET_GENERIC; - case INPUT_PRESET_CAMCORDER: - return NAME_INPUT_PRESET_CAMCORDER; - case INPUT_PRESET_VOICE_RECOGNITION: - return NAME_INPUT_PRESET_VOICE_RECOGNITION; - case INPUT_PRESET_VOICE_COMMUNICATION: - return NAME_INPUT_PRESET_VOICE_COMMUNICATION; - case INPUT_PRESET_UNPROCESSED: - return NAME_INPUT_PRESET_UNPROCESSED; - case INPUT_PRESET_VOICE_PERFORMANCE: - return NAME_INPUT_PRESET_VOICE_PERFORMANCE; - default: - return "Invalid"; - } - } - - private static boolean matchInputPreset(String text, int preset) { - return convertInputPresetToText(preset).toLowerCase().equals(text); - } - - /** - * Case insensitive. - * @param text - * @return inputPreset, eg. INPUT_PRESET_CAMCORDER - */ - public static int convertTextToInputPreset(String text) { - text = text.toLowerCase(); - if (matchInputPreset(text, INPUT_PRESET_GENERIC)) { - return INPUT_PRESET_GENERIC; - } else if (matchInputPreset(text, INPUT_PRESET_CAMCORDER)) { - return INPUT_PRESET_CAMCORDER; - } else if (matchInputPreset(text, INPUT_PRESET_VOICE_RECOGNITION)) { - return INPUT_PRESET_VOICE_RECOGNITION; - } else if (matchInputPreset(text, INPUT_PRESET_VOICE_COMMUNICATION)) { - return INPUT_PRESET_VOICE_COMMUNICATION; - } else if (matchInputPreset(text, INPUT_PRESET_UNPROCESSED)) { - return INPUT_PRESET_UNPROCESSED; - } else if (matchInputPreset(text, INPUT_PRESET_VOICE_PERFORMANCE)) { - return INPUT_PRESET_VOICE_PERFORMANCE; - } - return -1; - } - - public int getChannelCount() { - return mChannelCount; - } - - public void setChannelCount(int channelCount) { - this.mChannelCount = channelCount; - } - - public int getSampleRate() { - return mSampleRate; - } - - public void setSampleRate(int sampleRate) { - this.mSampleRate = sampleRate; - } - - public int getDeviceId() { - return mDeviceId; - } - - public void setDeviceId(int deviceId) { - this.mDeviceId = deviceId; - } - - public int getSessionId() { - return mSessionId; - } - - public void setSessionId(int sessionId) { - mSessionId = sessionId; - } - - public boolean isMMap() { - return mMMap; - } - public void setMMap(boolean b) { - mMMap = b; - } - - public int getNativeApi() { - return mNativeApi; - } - - public void setNativeApi(int nativeApi) { - mNativeApi = nativeApi; - } - - public void setChannelConversionAllowed(boolean b) { mChannelConversionAllowed = b; } - - public boolean getChannelConversionAllowed() { - return mChannelConversionAllowed; - } - - public void setFormatConversionAllowed(boolean b) { - mFormatConversionAllowed = b; - } - - public boolean getFormatConversionAllowed() { - return mFormatConversionAllowed; - } - - public void setRateConversionQuality(int quality) { mRateConversionQuality = quality; } - - public int getRateConversionQuality() { - return mRateConversionQuality; - } - -} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/StreamConfigurationView.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/StreamConfigurationView.java deleted file mode 100644 index 64aeb75f..00000000 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/StreamConfigurationView.java +++ /dev/null @@ -1,464 +0,0 @@ -/* - * Copyright 2017 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. - */ - -package com.google.sample.oboe.manualtest; - -import android.content.Context; -import android.media.AudioManager; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.AdapterView; -import android.widget.CheckBox; -import android.widget.Spinner; -import android.widget.TableLayout; -import android.widget.TableRow; -import android.widget.TextView; -import android.widget.LinearLayout; - -import com.google.sample.audio_device.AudioDeviceListEntry; -import com.google.sample.audio_device.AudioDeviceSpinner; - -import java.text.BreakIterator; - -/** - * View for Editing a requested StreamConfiguration - * and displaying the actual StreamConfiguration. - */ - -public class StreamConfigurationView extends LinearLayout { - - private StreamConfiguration mRequestedConfiguration; - private StreamConfiguration mActualConfiguration; - - protected Spinner mNativeApiSpinner; - private TextView mActualNativeApiView; - - private TextView mActualMMapView; - private CheckBox mRequestedMMapView; - private TextView mActualExclusiveView; - private TextView mActualPerformanceView; - private Spinner mPerformanceSpinner; - private CheckBox mRequestedExclusiveView; - private CheckBox mChannelConversionBox; - private CheckBox mFormatConversionBox; - private Spinner mChannelCountSpinner; - private TextView mActualChannelCountView; - private TextView mActualFormatView; - - private TextView mActualInputPresetView; - private Spinner mInputPresetSpinner; - private TableRow mInputPresetTableRow; - private Spinner mFormatSpinner; - private Spinner mSampleRateSpinner; - private Spinner mRateConversionQualitySpinner; - private TextView mActualSampleRateView; - private LinearLayout mHideableView; - - private AudioDeviceSpinner mDeviceSpinner; - private TextView mActualSessionIdView; - private CheckBox mRequestAudioEffect; - - private TextView mStreamInfoView; - private TextView mStreamStatusView; - private TextView mOptionExpander; - private String mHideSettingsText; - private String mShowSettingsText; - - // Create an anonymous implementation of OnClickListener - private View.OnClickListener mToggleListener = new View.OnClickListener() { - public void onClick(View v) { - if (mHideableView.isShown()) { - hideSettingsView(); - } else { - showSettingsView(); - } - } - }; - - public static String yesOrNo(boolean b) { - return b ? "YES" : "NO"; - } - - private void updateSettingsViewText() { - if (mHideableView.isShown()) { - mOptionExpander.setText(mHideSettingsText); - } else { - mOptionExpander.setText(mShowSettingsText); - } - } - - public void showSettingsView() { - mHideableView.setVisibility(View.VISIBLE); - updateSettingsViewText(); - } - - public void hideSampleRateMenu() { - if (mSampleRateSpinner != null) { - mSampleRateSpinner.setVisibility(View.GONE); - } - } - - public void hideSettingsView() { - mHideableView.setVisibility(View.GONE); - updateSettingsViewText(); - } - - public StreamConfigurationView(Context context) { - super(context); - initializeViews(context); - } - - public StreamConfigurationView(Context context, AttributeSet attrs) { - super(context, attrs); - initializeViews(context); - } - - public StreamConfigurationView(Context context, - AttributeSet attrs, - int defStyle) { - super(context, attrs, defStyle); - initializeViews(context); - } - - /** - * Inflates the views in the layout. - * - * @param context - * the current context for the view. - */ - private void initializeViews(Context context) { - LayoutInflater inflater = (LayoutInflater) context - .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - inflater.inflate(R.layout.stream_config, this); - - mHideSettingsText = getResources().getString(R.string.hint_hide_settings); - mShowSettingsText = getResources().getString(R.string.hint_show_settings); - - mHideableView = (LinearLayout) findViewById(R.id.hideableView); - - mOptionExpander = (TextView) findViewById(R.id.toggle_stream_config); - mOptionExpander.setOnClickListener(mToggleListener); - - mNativeApiSpinner = (Spinner) findViewById(R.id.spinnerNativeApi); - mNativeApiSpinner.setOnItemSelectedListener(new NativeApiSpinnerListener()); - mNativeApiSpinner.setSelection(StreamConfiguration.NATIVE_API_UNSPECIFIED); - - mActualNativeApiView = (TextView) findViewById(R.id.actualNativeApi); - - mChannelConversionBox = (CheckBox) findViewById(R.id.checkChannelConversion); - mChannelConversionBox.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - mRequestedConfiguration.setChannelConversionAllowed(mChannelConversionBox.isChecked()); - } - }); - - mFormatConversionBox = (CheckBox) findViewById(R.id.checkFormatConversion); - mFormatConversionBox.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - mRequestedConfiguration.setFormatConversionAllowed(mFormatConversionBox.isChecked()); - } - }); - - mActualMMapView = (TextView) findViewById(R.id.actualMMap); - mRequestedMMapView = (CheckBox) findViewById(R.id.requestedMMapEnable); - mRequestedMMapView.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - mRequestedConfiguration.setMMap(mRequestedMMapView.isChecked()); - } - }); - boolean mmapSupported = NativeEngine.isMMapSupported(); - mRequestedMMapView.setEnabled(mmapSupported); - mRequestedMMapView.setChecked(mmapSupported); - - mActualExclusiveView = (TextView) findViewById(R.id.actualExclusiveMode); - mRequestedExclusiveView = (CheckBox) findViewById(R.id.requestedExclusiveMode); - mRequestedExclusiveView.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - mRequestedConfiguration.setSharingMode(mRequestedExclusiveView.isChecked() - ? StreamConfiguration.SHARING_MODE_EXCLUSIVE - : StreamConfiguration.SHARING_MODE_SHARED); - } - }); - - boolean mmapExclusiveSupported = NativeEngine.isMMapExclusiveSupported(); - mRequestedExclusiveView.setEnabled(mmapExclusiveSupported); - mRequestedExclusiveView.setChecked(mmapExclusiveSupported); - - mActualSessionIdView = (TextView) findViewById(R.id.sessionId); - mRequestAudioEffect = (CheckBox) findViewById(R.id.requestAudioEffect); - mRequestAudioEffect.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - mRequestedConfiguration.setSessionId(mRequestAudioEffect.isChecked() - ? StreamConfiguration.SESSION_ID_ALLOCATE - : StreamConfiguration.SESSION_ID_NONE); - } - }); - - mActualSampleRateView = (TextView) findViewById(R.id.actualSampleRate); - mSampleRateSpinner = (Spinner) findViewById(R.id.spinnerSampleRate); - mSampleRateSpinner.setOnItemSelectedListener(new SampleRateSpinnerListener()); - - mActualChannelCountView = (TextView) findViewById(R.id.actualChannelCount); - mChannelCountSpinner = (Spinner) findViewById(R.id.spinnerChannelCount); - mChannelCountSpinner.setOnItemSelectedListener(new ChannelCountSpinnerListener()); - - mActualFormatView = (TextView) findViewById(R.id.actualAudioFormat); - mFormatSpinner = (Spinner) findViewById(R.id.spinnerFormat); - mFormatSpinner.setOnItemSelectedListener(new FormatSpinnerListener()); - - mRateConversionQualitySpinner = (Spinner) findViewById(R.id.spinnerSRCQuality); - mRateConversionQualitySpinner.setOnItemSelectedListener(new RateConversionQualitySpinnerListener()); - - mActualPerformanceView = (TextView) findViewById(R.id.actualPerformanceMode); - mPerformanceSpinner = (Spinner) findViewById(R.id.spinnerPerformanceMode); - mPerformanceSpinner.setOnItemSelectedListener(new PerformanceModeSpinnerListener()); - mPerformanceSpinner.setSelection(StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY - - StreamConfiguration.PERFORMANCE_MODE_NONE); - - mInputPresetTableRow = (TableRow) findViewById(R.id.rowInputPreset); - mActualInputPresetView = (TextView) findViewById(R.id.actualInputPreset); - mInputPresetSpinner = (Spinner) findViewById(R.id.spinnerInputPreset); - mInputPresetSpinner.setOnItemSelectedListener(new InputPresetSpinnerListener()); - mInputPresetSpinner.setSelection(2); // TODO need better way to select voice recording default - - mStreamInfoView = (TextView) findViewById(R.id.streamInfo); - - mStreamStatusView = (TextView) findViewById(R.id.statusView); - - mDeviceSpinner = (AudioDeviceSpinner) findViewById(R.id.devices_spinner); - mDeviceSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView adapterView, View view, int i, long l) { - int id = ((AudioDeviceListEntry) mDeviceSpinner.getSelectedItem()).getId(); - mRequestedConfiguration.setDeviceId(id); - } - - @Override - public void onNothingSelected(AdapterView adapterView) { - mRequestedConfiguration.setDeviceId(StreamConfiguration.UNSPECIFIED); - } - }); - - showSettingsView(); - } - - public void setOutput(boolean output) { - String ioText; - if (output) { - mDeviceSpinner.setDirectionType(AudioManager.GET_DEVICES_OUTPUTS); - ioText = "OUTPUT"; - } else { - mDeviceSpinner.setDirectionType(AudioManager.GET_DEVICES_INPUTS); - ioText = "INPUT"; - } - mHideSettingsText = getResources().getString(R.string.hint_hide_settings) + " - " + ioText; - mShowSettingsText = getResources().getString(R.string.hint_show_settings) + " - " + ioText; - updateSettingsViewText(); - - // Don't show InputPresets for output streams. - mInputPresetTableRow.setVisibility(output ? View.GONE : View.VISIBLE); - } - - private class NativeApiSpinnerListener implements android.widget.AdapterView.OnItemSelectedListener { - @Override - public void onItemSelected(AdapterView parent, View view, int pos, long id) { - mRequestedConfiguration.setNativeApi(pos); - } - - @Override - public void onNothingSelected(AdapterView parent) { - mRequestedConfiguration.setNativeApi(StreamConfiguration.NATIVE_API_UNSPECIFIED); - } - } - - private class PerformanceModeSpinnerListener implements android.widget.AdapterView.OnItemSelectedListener { - @Override - public void onItemSelected(AdapterView parent, View view, int performanceMode, long id) { - mRequestedConfiguration.setPerformanceMode(performanceMode - + StreamConfiguration.PERFORMANCE_MODE_NONE); - } - - @Override - public void onNothingSelected(AdapterView parent) { - mRequestedConfiguration.setPerformanceMode(StreamConfiguration.PERFORMANCE_MODE_NONE); - } - } - - private class ChannelCountSpinnerListener implements android.widget.AdapterView.OnItemSelectedListener { - @Override - public void onItemSelected(AdapterView parent, View view, int pos, long id) { - mRequestedConfiguration.setChannelCount(pos); - } - - @Override - public void onNothingSelected(AdapterView parent) { - mRequestedConfiguration.setChannelCount(StreamConfiguration.UNSPECIFIED); - } - } - - private class SampleRateSpinnerListener implements android.widget.AdapterView.OnItemSelectedListener { - @Override - public void onItemSelected(AdapterView parent, View view, int pos, long id) { - String text = parent.getItemAtPosition(pos).toString(); - int sampleRate = Integer.parseInt(text); - mRequestedConfiguration.setSampleRate(sampleRate); - } - - @Override - public void onNothingSelected(AdapterView parent) { - mRequestedConfiguration.setSampleRate(StreamConfiguration.UNSPECIFIED); - } - } - - private class FormatSpinnerListener implements android.widget.AdapterView.OnItemSelectedListener { - @Override - public void onItemSelected(AdapterView parent, View view, int pos, long id) { - // Menu position matches actual enum value! - mRequestedConfiguration.setFormat(pos); - } - - @Override - public void onNothingSelected(AdapterView parent) { - mRequestedConfiguration.setFormat(StreamConfiguration.UNSPECIFIED); - } - } - - private class InputPresetSpinnerListener implements android.widget.AdapterView.OnItemSelectedListener { - @Override - public void onItemSelected(AdapterView parent, View view, int pos, long id) { - String text = parent.getItemAtPosition(pos).toString(); - int inputPreset = StreamConfiguration.convertTextToInputPreset(text); - mRequestedConfiguration.setInputPreset(inputPreset); - } - - @Override - public void onNothingSelected(AdapterView parent) { - mRequestedConfiguration.setInputPreset(StreamConfiguration.INPUT_PRESET_GENERIC); - } - } - - private class RateConversionQualitySpinnerListener - implements android.widget.AdapterView.OnItemSelectedListener { - @Override - public void onItemSelected(AdapterView parent, View view, int pos, long id) { - // Menu position matches actual enum value! - mRequestedConfiguration.setRateConversionQuality(pos); - } - - @Override - public void onNothingSelected(AdapterView parent) { - mRequestedConfiguration.setRateConversionQuality(StreamConfiguration.RATE_CONVERSION_QUALITY_HIGH); - } - } - - public void setChildrenEnabled(boolean enabled) { - mNativeApiSpinner.setEnabled(enabled); - mPerformanceSpinner.setEnabled(enabled); - mRequestedExclusiveView.setEnabled(enabled); - mSampleRateSpinner.setEnabled(enabled); - mChannelCountSpinner.setEnabled(enabled); - mFormatSpinner.setEnabled(enabled); - mDeviceSpinner.setEnabled(enabled); - mRequestAudioEffect.setEnabled(enabled); - } - - // This must be called on the UI thread. - void updateDisplay() { - int value; - - value = mActualConfiguration.getNativeApi(); - mActualNativeApiView.setText(StreamConfiguration.convertNativeApiToText(value)); - - mActualMMapView.setText(yesOrNo(mActualConfiguration.isMMap())); - int sharingMode = mActualConfiguration.getSharingMode(); - boolean isExclusive = (sharingMode == StreamConfiguration.SHARING_MODE_EXCLUSIVE); - mActualExclusiveView.setText(yesOrNo(isExclusive)); - - value = mActualConfiguration.getPerformanceMode(); - mActualPerformanceView.setText(StreamConfiguration.convertPerformanceModeToText(value)); - mActualPerformanceView.requestLayout(); - - value = mActualConfiguration.getFormat(); - mActualFormatView.setText(StreamConfiguration.convertFormatToText(value)); - mActualFormatView.requestLayout(); - - value = mActualConfiguration.getInputPreset(); - mActualInputPresetView.setText(StreamConfiguration.convertInputPresetToText(value)); - mActualInputPresetView.requestLayout(); - - mActualChannelCountView.setText(mActualConfiguration.getChannelCount() + ""); - mActualSampleRateView.setText(mActualConfiguration.getSampleRate() + ""); - mActualSessionIdView.setText("S#: " + mActualConfiguration.getSessionId()); - - boolean isMMap = mActualConfiguration.isMMap(); - mStreamInfoView.setText("burst = " + mActualConfiguration.getFramesPerBurst() - + ", capacity = " + mActualConfiguration.getBufferCapacityInFrames() - + ", devID = " + mActualConfiguration.getDeviceId() - + ", " + (mActualConfiguration.isMMap() ? "MMAP" : "Legacy") - + (isMMap ? ", " + StreamConfiguration.convertSharingModeToText(sharingMode) : "") - ); - - mHideableView.requestLayout(); - } - - // This must be called on the UI thread. - public void setStatusText(String msg) { - mStreamStatusView.setText(msg); - } - - protected StreamConfiguration getRequestedConfiguration() { - return mRequestedConfiguration; - } - - public void setRequestedConfiguration(StreamConfiguration configuration) { - mRequestedConfiguration = configuration; - if (configuration != null) { - mRateConversionQualitySpinner.setSelection(configuration.getRateConversionQuality()); - mChannelConversionBox.setChecked(configuration.getChannelConversionAllowed()); - mFormatConversionBox.setChecked(configuration.getFormatConversionAllowed()); - } - } - - protected StreamConfiguration getActualConfiguration() { - return mActualConfiguration; - } - public void setActualConfiguration(StreamConfiguration configuration) { - mActualConfiguration = configuration; - } - - public void setExclusiveMode(boolean b) { - mRequestedExclusiveView.setChecked(b); - mRequestedConfiguration.setSharingMode(b - ? StreamConfiguration.SHARING_MODE_EXCLUSIVE - : StreamConfiguration.SHARING_MODE_SHARED); - } - - public void setFormat(int format) { - mFormatSpinner.setSelection(format); // position matches format - mRequestedConfiguration.setFormat(format); - } - - public void setFormatConversionAllowed(boolean allowed) { - mFormatConversionBox.setChecked(allowed); - mRequestedConfiguration.setFormatConversionAllowed(allowed); - } -} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TapToToneActivity.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TapToToneActivity.java deleted file mode 100644 index bc5b90ac..00000000 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TapToToneActivity.java +++ /dev/null @@ -1,386 +0,0 @@ -/* - * Copyright 2015 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. - */ - -package com.google.sample.oboe.manualtest; - -import android.Manifest; -import android.content.pm.PackageManager; -import android.media.midi.MidiDevice; -import android.media.midi.MidiDeviceInfo; -import android.media.midi.MidiInputPort; -import android.media.midi.MidiManager; -import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; -import android.util.Log; -import android.view.Menu; -import android.view.MenuItem; -import android.view.MotionEvent; -import android.view.View; -import android.widget.TextView; -import android.widget.Toast; - -import com.mobileer.miditools.MidiOutputPortConnectionSelector; -import com.mobileer.miditools.MidiPortConnector; -import com.mobileer.miditools.MidiTools; - -import java.io.IOException; - -import static com.google.sample.oboe.manualtest.AudioMidiTester.TestListener; -import static com.google.sample.oboe.manualtest.AudioMidiTester.TestResult; - -public class TapToToneActivity extends TestOutputActivityBase { - private static final int MY_PERMISSIONS_REQUEST_RECORD_AUDIO = 1234; - private TextView mResultView; - private MidiManager mMidiManager; - private MidiInputPort mInputPort; - - protected AudioMidiTester mAudioMidiTester; - - private MidiOutputPortConnectionSelector mPortSelector; - private MyTestListener mTestListener = new MyTestListener(); - private WaveformView mWaveformView; - // Stats for latency - private int mMeasurementCount; - private int mLatencySumSamples; - private int mLatencyMin; - private int mLatencyMax; - - @Override - protected void inflateActivity() { - setContentView(R.layout.activity_tap_to_tone); - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - mAudioOutTester = addAudioOutputTester(); - - mResultView = (TextView) findViewById(R.id.resultView); - - if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_MIDI)) { - setupMidi(); - } else { - Toast.makeText(TapToToneActivity.this, - "MIDI not supported!", Toast.LENGTH_LONG) - .show(); - } - - mWaveformView = (WaveformView) findViewById(R.id.waveview_audio); - - // Start a blip test when the waveform view is tapped. - mWaveformView.setOnTouchListener(new View.OnTouchListener() { - @Override - public boolean onTouch(View view, MotionEvent event) { - int action = event.getActionMasked(); - switch (action) { - case MotionEvent.ACTION_DOWN: - case MotionEvent.ACTION_POINTER_DOWN: - mAudioMidiTester.setEnabled(true); - break; - case MotionEvent.ACTION_MOVE: - break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_POINTER_UP: - mAudioMidiTester.setEnabled(false); - break; - } - // Must return true or we do not get the ACTION_MOVE and - // ACTION_UP events. - return true; - } - }); - - updateEnabledWidgets(); - } - - @Override - protected void onStart() { - super.onStart(); - setActivityType(ACTIVITY_TAP_TO_TONE); - } - - @Override - protected void onDestroy() { - mAudioMidiTester.removeTestListener(mTestListener); - closeMidiResources(); - super.onDestroy(); - } - - private void setupMidi() { - // Setup MIDI - mMidiManager = (MidiManager) getSystemService(MIDI_SERVICE); - MidiDeviceInfo[] infos = mMidiManager.getDevices(); - - // Open the port now so that the AudioMidiTester gets created. - for (MidiDeviceInfo info : infos) { - Bundle properties = info.getProperties(); - String product = properties - .getString(MidiDeviceInfo.PROPERTY_PRODUCT); - - Log.i(TAG, "product = " + product); - if ("AudioLatencyTester".equals(product)) { - openPort(info); - break; - } - } - - } - - // These should only be set after mAudioMidiTester is set. - private void setSpinnerListeners() { - MidiDeviceInfo synthInfo = MidiTools.findDevice(mMidiManager, "AndroidTest", - "AudioLatencyTester"); - Log.i(TAG, "found tester virtual device info: " + synthInfo); - int portIndex = 0; - mPortSelector = new MidiOutputPortConnectionSelector(mMidiManager, this, - R.id.spinner_synth_sender, synthInfo, portIndex); - mPortSelector.setConnectedListener(new MyPortsConnectedListener()); - - } - - private class MyTestListener implements TestListener { - @Override - public void onTestFinished(final TestResult result) { - runOnUiThread(new Runnable() { - public void run() { - showTestResults(result); - } - }); - } - - @Override - public void onNoteOn(final int pitch) { - runOnUiThread(new Runnable() { - public void run() { - mStreamContexts.get(0).configurationView.setStatusText("MIDI pitch = " + pitch); - } - }); - } - } - - // Runs on UI thread. - private void showTestResults(TestResult result) { - String text; - int previous = 0; - if (result == null) { - text = ""; - mWaveformView.clearSampleData(); - } else { - if (result.events.length < 2) { - text = "Not enough edges. Use fingernail.\n"; - mWaveformView.setCursorData(null); - } else if (result.events.length > 2) { - text = "Too many edges.\n"; - mWaveformView.setCursorData(null); - } else { - int[] cursors = new int[2]; - cursors[0] = result.events[0].sampleIndex; - cursors[1] = result.events[1].sampleIndex; - int latencySamples = cursors[1] - cursors[0]; - mLatencySumSamples += latencySamples; - mMeasurementCount++; - - int latencyMillis = 1000 * latencySamples / result.frameRate; - if (mLatencyMin > latencyMillis) { - mLatencyMin = latencyMillis; - } - if (mLatencyMax < latencyMillis) { - mLatencyMax = latencyMillis; - } - - text = String.format("latency = %3d msec\n", latencyMillis); - mWaveformView.setCursorData(cursors); - } - mWaveformView.setSampleData(result.filtered); - } - - if (mMeasurementCount > 0) { - int averageLatencySamples = mLatencySumSamples / mMeasurementCount; - int averageLatencyMillis = 1000 * averageLatencySamples / result.frameRate; - final String plural = (mMeasurementCount == 1) ? "test" : "tests"; - text = text + String.format("min = %3d, avg = %3d, max = %3d, %d %s", - mLatencyMin, averageLatencyMillis, mLatencyMax, mMeasurementCount, plural); - } - final String postText = text; - mWaveformView.post(new Runnable() { - public void run() { - mResultView.setText(postText); - mWaveformView.postInvalidate(); - } - }); - } - - private void openPort(final MidiDeviceInfo info) { - mMidiManager.openDevice(info, new MidiManager.OnDeviceOpenedListener() { - @Override - public void onDeviceOpened(MidiDevice device) { - if (device == null) { - Log.e(TAG, "could not open device " + info); - } else { - mInputPort = device.openInputPort(0); - Log.i(TAG, "opened MIDI port = " + mInputPort + " on " + info); - mAudioMidiTester = AudioMidiTester.getInstance(); - - Log.i(TAG, "openPort() mAudioMidiTester = " + mAudioMidiTester); - // Now that we have created the AudioMidiTester, close the port so we can - // open it later. - try { - mInputPort.close(); - } catch (IOException e) { - e.printStackTrace(); - } - mAudioMidiTester.addTestListener(mTestListener); - - setSpinnerListeners(); - } - } - }, new Handler(Looper.getMainLooper()) - ); - } - - // TODO Listen to the synth server - // for open/close events and then disable/enable the spinner. - private class MyPortsConnectedListener - implements MidiPortConnector.OnPortsConnectedListener { - @Override - public void onPortsConnected(final MidiDevice.MidiConnection connection) { - Log.i(TAG, "onPortsConnected, connection = " + connection); - runOnUiThread(new Runnable() { - @Override - public void run() { - if (connection == null) { - Toast.makeText(TapToToneActivity.this, - R.string.error_port_busy, Toast.LENGTH_LONG) - .show(); - mPortSelector.clearSelection(); - } else { - Toast.makeText(TapToToneActivity.this, - R.string.port_open_ok, Toast.LENGTH_LONG) - .show(); - } - } - }); - } - } - - private void closeMidiResources() { - if (mPortSelector != null) { - mPortSelector.close(); - } - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - // Inflate the menu; this adds items to the action bar if it is present. - getMenuInflater().inflate(R.menu.menu_main, menu); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - // Handle action bar item clicks here. The action bar will - // automatically handle clicks on the Home/Up button, so long - // as you specify a parent activity in AndroidManifest.xml. - int id = item.getItemId(); - - //noinspection SimplifiableIfStatement - if (id == R.id.action_settings) { - return true; - } - - return super.onOptionsItemSelected(item); - } - - private boolean hasRecordAudioPermission(){ - boolean hasPermission = (checkSelfPermission( - Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED); - Log.i(TAG, "Has RECORD_AUDIO permission? " + hasPermission); - return hasPermission; - } - - private void requestRecordAudioPermission(){ - - String requiredPermission = Manifest.permission.RECORD_AUDIO; - - // If the user previously denied this permission then show a message explaining why - // this permission is needed - if (shouldShowRequestPermissionRationale(requiredPermission)) { - showErrorToast("This app needs to record audio through the microphone...."); - } - - // request the permission. - requestPermissions(new String[]{requiredPermission}, - MY_PERMISSIONS_REQUEST_RECORD_AUDIO); - } - @Override - public void onRequestPermissionsResult(int requestCode, - String permissions[], int[] grantResults) { - // TODO - } - - @Override - public void startAudio() { - if (hasRecordAudioPermission()) { - startAudioPermitted(); - } else { - requestRecordAudioPermission(); - } - } - - private void startAudioPermitted() { - super.startAudio(); - resetLatency(); - try { - mAudioMidiTester.start(); - if (mAudioOutTester != null) { - mAudioOutTester.setToneType(OboeAudioOutputStream.TONE_TYPE_SAW_PING); - } else { - Log.w(TAG, "startAudioPermitted, mAudioOutTester = null, cannot setToneType(ping)"); - } - } catch (IOException e) { - e.printStackTrace(); - } - } - - @Override - public void stopAudio() { - mAudioMidiTester.stop(); - super.stopAudio(); - } - - @Override - public void pauseAudio() { - mAudioMidiTester.stop(); - super.pauseAudio(); - } - - @Override - public void closeAudio() { - mAudioMidiTester.stop(); - super.closeAudio(); - } - - private void resetLatency() { - mMeasurementCount = 0; - mLatencySumSamples = 0; - mLatencyMin = Integer.MAX_VALUE; - mLatencyMax = 0; - showTestResults(null); - } - -} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestAudioActivity.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestAudioActivity.java deleted file mode 100644 index 317b07bb..00000000 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestAudioActivity.java +++ /dev/null @@ -1,486 +0,0 @@ -/* - * Copyright 2017 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. - */ - -package com.google.sample.oboe.manualtest; - -import android.app.Activity; -import android.content.Context; -import android.media.AudioManager; -import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; -import android.util.Log; -import android.view.View; -import android.view.WindowManager; -import android.widget.Button; -import android.widget.CheckBox; -import android.widget.Toast; - -import java.io.IOException; -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Date; - -/** - * Base class for other Activities. - */ -abstract class TestAudioActivity extends Activity { - public static final String TAG = "TestOboe"; - - protected static final int FADER_PROGRESS_MAX = 1000; - - public static final int AUDIO_STATE_OPEN = 0; - public static final int AUDIO_STATE_STARTED = 1; - public static final int AUDIO_STATE_PAUSED = 2; - public static final int AUDIO_STATE_STOPPED = 3; - public static final int AUDIO_STATE_CLOSED = 4; - - public static final int COLOR_ACTIVE = 0xFFD0D0A0; - public static final int COLOR_IDLE = 0xFFD0D0D0; - - // Pass the activity index to native so it can know how to respond to the start and stop calls. - // WARNING - must match definitions in NativeAudioContext.h ActivityType - public static final int ACTIVITY_TEST_OUTPUT = 0; - public static final int ACTIVITY_TEST_INPUT = 1; - public static final int ACTIVITY_TAP_TO_TONE = 2; - public static final int ACTIVITY_RECORD_PLAY = 3; - public static final int ACTIVITY_ECHO = 4; - public static final int ACTIVITY_RT_LATENCY = 5; - public static final int ACTIVITY_GLITCHES = 6; - public static final int ACTIVITY_TEST_DISCONNECT = 7; - - private int mAudioState = AUDIO_STATE_CLOSED; - protected String audioManagerSampleRate; - protected int audioManagerFramesPerBurst; - protected ArrayList mStreamContexts; - private Button mOpenButton; - private Button mStartButton; - private Button mPauseButton; - private Button mStopButton; - private Button mCloseButton; - private MyStreamSniffer mStreamSniffer; - private CheckBox mCallbackReturnStopBox; - private int mSampleRate; - - public static class StreamContext { - StreamConfigurationView configurationView; - AudioStreamTester tester; - - boolean isInput() { - return tester.getCurrentAudioStream().isInput(); - } - } - - // Periodically query the status of the streams. - protected class MyStreamSniffer { - public static final int SNIFFER_UPDATE_PERIOD_MSEC = 150; - public static final int SNIFFER_UPDATE_DELAY_MSEC = 300; - - private Handler mHandler; - - // Display status info for the stream. - private Runnable runnableCode = new Runnable() { - @Override - public void run() { - boolean streamClosed = false; - boolean gotViews = false; - for (StreamContext streamContext : mStreamContexts) { - AudioStreamBase.StreamStatus status = streamContext.tester.getCurrentAudioStream().getStreamStatus(); - if (streamContext.configurationView != null) { - // Handler runs this on the main UI thread. - int framesPerBurst = streamContext.tester.getCurrentAudioStream().getFramesPerBurst(); - status.framesPerCallback = getFramesPerCallback(); - final String msg = status.dump(framesPerBurst); - streamContext.configurationView.setStatusText(msg); - updateStreamDisplay(); - gotViews = true; - } - - streamClosed = streamClosed || (status.state >= 12); - } - - if (streamClosed) { - onStreamClosed(); - } else { - // Repeat this runnable code block again. - if (gotViews) { - mHandler.postDelayed(runnableCode, SNIFFER_UPDATE_PERIOD_MSEC); - } - } - } - }; - - private void startStreamSniffer() { - stopStreamSniffer(); - mHandler = new Handler(Looper.getMainLooper()); - // Start the initial runnable task by posting through the handler - mHandler.postDelayed(runnableCode, SNIFFER_UPDATE_DELAY_MSEC); - } - - private void stopStreamSniffer() { - if (mHandler != null) { - mHandler.removeCallbacks(runnableCode); - } - } - - } - - public void onStreamClosed() { - } - - protected abstract void inflateActivity(); - - void updateStreamDisplay() { - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - inflateActivity(); - findAudioCommon(); - } - - public void hideSettingsViews() { - for (StreamContext streamContext : mStreamContexts) { - if (streamContext.configurationView != null) { - streamContext.configurationView.hideSettingsView(); - } - } - } - - @Override - protected void onStart() { - super.onStart(); - resetConfiguration(); - } - - protected void resetConfiguration() { - } - - @Override - protected void onStop() { - Log.i(TAG, "onStop() called so stopping audio ========================="); - stopAudio(); - closeAudio(); - super.onStop(); - } - - @Override - protected void onDestroy() { - mAudioState = AUDIO_STATE_CLOSED; - super.onDestroy(); - } - - protected void updateEnabledWidgets() { - if (mOpenButton != null) { - mOpenButton.setBackgroundColor(mAudioState == AUDIO_STATE_OPEN ? COLOR_ACTIVE : COLOR_IDLE); - mStartButton.setBackgroundColor(mAudioState == AUDIO_STATE_STARTED ? COLOR_ACTIVE : COLOR_IDLE); - mPauseButton.setBackgroundColor(mAudioState == AUDIO_STATE_PAUSED ? COLOR_ACTIVE : COLOR_IDLE); - mStopButton.setBackgroundColor(mAudioState == AUDIO_STATE_STOPPED ? COLOR_ACTIVE : COLOR_IDLE); - mCloseButton.setBackgroundColor(mAudioState == AUDIO_STATE_CLOSED ? COLOR_ACTIVE : COLOR_IDLE); - } - setConfigViewsEnabled(mAudioState == AUDIO_STATE_CLOSED); - } - - private void setConfigViewsEnabled(boolean b) { - for (StreamContext streamContext : mStreamContexts) { - if (streamContext.configurationView != null) { - streamContext.configurationView.setChildrenEnabled(b); - } - } - } - - abstract boolean isOutput(); - - public void clearStreamContexts() { - mStreamContexts.clear(); - } - - public StreamContext addOutputStreamContext() { - StreamContext streamContext = new StreamContext(); - streamContext.tester = AudioOutputTester.getInstance(); - streamContext.configurationView = (StreamConfigurationView) - findViewById(R.id.outputStreamConfiguration); - if (streamContext.configurationView == null) { - streamContext.configurationView = (StreamConfigurationView) - findViewById(R.id.streamConfiguration); - } - if (streamContext.configurationView != null) { - streamContext.configurationView.setOutput(true); - streamContext.configurationView.setRequestedConfiguration(streamContext.tester.requestedConfiguration); - streamContext.configurationView.setActualConfiguration(streamContext.tester.actualConfiguration); - } - mStreamContexts.add(streamContext); - return streamContext; - } - - - public AudioOutputTester addAudioOutputTester() { - StreamContext streamContext = addOutputStreamContext(); - return (AudioOutputTester) streamContext.tester; - } - - public StreamContext addInputStreamContext() { - StreamContext streamContext = new StreamContext(); - streamContext.tester = AudioInputTester.getInstance(); - streamContext.configurationView = (StreamConfigurationView) - findViewById(R.id.inputStreamConfiguration); - if (streamContext.configurationView == null) { - streamContext.configurationView = (StreamConfigurationView) - findViewById(R.id.streamConfiguration); - } - if (streamContext.configurationView != null) { - streamContext.configurationView.setOutput(false); - streamContext.configurationView.setRequestedConfiguration(streamContext.tester.requestedConfiguration); - streamContext.configurationView.setActualConfiguration(streamContext.tester.actualConfiguration); - } - streamContext.tester = AudioInputTester.getInstance(); - mStreamContexts.add(streamContext); - return streamContext; - } - - public AudioInputTester addAudioInputTester() { - StreamContext streamContext = addInputStreamContext(); - return (AudioInputTester) streamContext.tester; - } - - void updateStreamConfigurationViews() { - for (StreamContext streamContext : mStreamContexts) { - if (streamContext.configurationView != null) { - streamContext.configurationView.updateDisplay(); - } - } - } - - StreamContext getFirstInputStreamContext() { - for (StreamContext streamContext : mStreamContexts) { - if (streamContext.isInput()) - return streamContext; - } - return null; - } - - StreamContext getFirstOutputStreamContext() { - for (StreamContext streamContext : mStreamContexts) { - if (!streamContext.isInput()) - return streamContext; - } - return null; - } - - protected void findAudioCommon() { - mOpenButton = (Button) findViewById(R.id.button_open); - if (mOpenButton != null) { - mStartButton = (Button) findViewById(R.id.button_start); - mPauseButton = (Button) findViewById(R.id.button_pause); - mStopButton = (Button) findViewById(R.id.button_stop); - mCloseButton = (Button) findViewById(R.id.button_close); - } - mStreamContexts = new ArrayList(); - - queryNativeAudioParameters(); - - mCallbackReturnStopBox = (CheckBox) findViewById(R.id.callbackReturnStop); - if (mCallbackReturnStopBox != null) { - mCallbackReturnStopBox.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - OboeAudioStream.setCallbackReturnStop(mCallbackReturnStopBox.isChecked()); - } - }); - } - OboeAudioStream.setCallbackReturnStop(false); - - mStreamSniffer = new MyStreamSniffer(); - } - - private void queryNativeAudioParameters() { - AudioManager myAudioMgr = (AudioManager) getSystemService(Context.AUDIO_SERVICE); - audioManagerSampleRate = myAudioMgr.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE); - String audioManagerFramesPerBurstText = myAudioMgr.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER); - audioManagerFramesPerBurst = Integer.parseInt(audioManagerFramesPerBurstText); - } - - abstract public void setupEffects(int sessionId); - - protected void showErrorToast(String message) { - showToast("Error: " + message); - } - - protected void showToast(final String message) { - runOnUiThread(new Runnable() { - @Override - public void run() { - Toast.makeText(TestAudioActivity.this, - message, - Toast.LENGTH_SHORT).show(); - } - }); - } - - public void openAudio(View view) { - try { - openAudio(); - } catch (Exception e) { - showErrorToast(e.getMessage()); - } - } - - public void startAudio(View view) { - Log.i(TAG, "startAudio() called ======================================="); - startAudio(); - keepScreenOn(true); - } - - protected void keepScreenOn(boolean on) { - if (on) { - getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - } else { - getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - } - } - - public void stopAudio(View view) { - stopAudio(); - keepScreenOn(false); - } - - public void pauseAudio(View view) { - pauseAudio(); - keepScreenOn(false); - } - - public void closeAudio(View view) { - closeAudio(); - } - - public int getSampleRate() { - return mSampleRate; - } - - public void openAudio() throws IOException { - closeAudio(); - - int sampleRate = 0; - - // Open output streams then open input streams. - // This is so that the capacity of input stream can be expanded to - // match the burst size of the output for full duplex. - for (StreamContext streamContext : mStreamContexts) { - if (!streamContext.isInput()) { - openStreamContext(streamContext); - int streamSampleRate = streamContext.tester.actualConfiguration.getSampleRate(); - if (sampleRate == 0) { - sampleRate = streamSampleRate; - } - } - } - for (StreamContext streamContext : mStreamContexts) { - if (streamContext.isInput()) { - if (sampleRate != 0) { - streamContext.tester.requestedConfiguration.setSampleRate(sampleRate); - } - openStreamContext(streamContext); - } - } - updateEnabledWidgets(); - mStreamSniffer.startStreamSniffer(); - } - - private void openStreamContext(StreamContext streamContext) throws IOException { - StreamConfiguration requestedConfig = streamContext.tester.requestedConfiguration; - StreamConfiguration actualConfig = streamContext.tester.actualConfiguration; - - requestedConfig.setFramesPerBurst(audioManagerFramesPerBurst); - streamContext.tester.open(); - mSampleRate = actualConfig.getSampleRate(); - mAudioState = AUDIO_STATE_OPEN; - int sessionId = actualConfig.getSessionId(); - if (sessionId > 0) { - setupEffects(sessionId); - } - if (streamContext.configurationView != null) { - streamContext.configurationView.updateDisplay(); - } - } - - // Native methods - private native int startNative(); - private native int pauseNative(); - private native int stopNative(); - protected native void setActivityType(int activityType); - private native int getFramesPerCallback(); - - public void startAudio() { - int result = startNative(); - if (result < 0) { - showErrorToast("Start failed with " + result); - } else { - for (StreamContext streamContext : mStreamContexts) { - StreamConfigurationView configView = streamContext.configurationView; - if (configView != null) { - configView.updateDisplay(); - } - } - mAudioState = AUDIO_STATE_STARTED; - updateEnabledWidgets(); - } - } - - public void pauseAudio() { - int result = pauseNative(); - if (result < 0) { - showErrorToast("Pause failed with " + result); - } else { - mAudioState = AUDIO_STATE_PAUSED; - updateEnabledWidgets(); - } - } - - public void stopAudio() { - int result = stopNative(); - if (result < 0) { - showErrorToast("Stop failed with " + result); - } else { - mAudioState = AUDIO_STATE_STOPPED; - updateEnabledWidgets(); - } - } - - public void stopAudioQuiet() { - stopNative(); - mAudioState = AUDIO_STATE_STOPPED; - updateEnabledWidgets(); - } - - public void closeAudio() { - mStreamSniffer.stopStreamSniffer(); - for (StreamContext streamContext : mStreamContexts) { - streamContext.tester.close(); - } - mAudioState = AUDIO_STATE_CLOSED; - updateEnabledWidgets(); - } - - String getTimestampString() { - DateFormat df = new SimpleDateFormat("yyyyMMdd-HHmmss"); - Date now = Calendar.getInstance().getTime(); - return df.format(now); - } - -} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestDisconnectActivity.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestDisconnectActivity.java deleted file mode 100644 index 56d61a48..00000000 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestDisconnectActivity.java +++ /dev/null @@ -1,482 +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. - */ - -package com.google.sample.oboe.manualtest; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.os.Build; -import android.os.Bundle; -import android.text.method.ScrollingMovementMethod; -import android.view.View; -import android.widget.Button; -import android.widget.TextView; - -import java.io.IOException; -import java.util.Date; - -/** - * Guide the user through a series of tests plugging in and unplugging a headset. - * Print a summary at the end of any failures. - * - * TODO Test Input - */ -public class TestDisconnectActivity extends TestAudioActivity implements Runnable { - - private static final String TEXT_SKIP = "SKIP"; - private static final String TEXT_PASS = "PASS"; - private static final String TEXT_FAIL = "FAIL !!!!"; - public static final int POLL_DURATION_MILLIS = 50; - public static final int SETTLING_TIME_MILLIS = 600; - public static final int TIME_TO_FAILURE_MILLIS = 3000; - - private TextView mInstructionsTextView; - private TextView mAutoTextView; - private TextView mStatusTextView; - private TextView mPlugTextView; - - private Thread mAutoThread; - private volatile boolean mThreadEnabled; - private volatile boolean mTestFailed; - private volatile boolean mSkipTest; - private volatile int mPlugCount; - private int mTestCount; - private StringBuffer mFailedSummary; - private int mPassCount; - private int mFailCount; - private BroadcastReceiver mPluginReceiver = new PluginBroadcastReceiver(); - private Button mStartButton; - private Button mStopButton; - private Button mShareButton; - private Button mFailButton; - private Button mSkipButton; - - // Receive a broadcast Intent when a headset is plugged in or unplugged. - // Display a count on screen. - public class PluginBroadcastReceiver extends BroadcastReceiver { - @Override - public void onReceive(Context context, Intent intent) { - mPlugCount++; - runOnUiThread(new Runnable() { - @Override - public void run() { - String message = "Intent.HEADSET_PLUG #" + mPlugCount; - mPlugTextView.setText(message); - } - }); - } - } - - @Override - protected void inflateActivity() { - setContentView(R.layout.activity_test_disconnect); - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - mInstructionsTextView = (TextView) findViewById(R.id.text_instructions); - mStatusTextView = (TextView) findViewById(R.id.text_status); - mPlugTextView = (TextView) findViewById(R.id.text_plug_events); - mAutoTextView = (TextView) findViewById(R.id.text_log); - mAutoTextView.setMovementMethod(new ScrollingMovementMethod()); - - mStartButton = (Button) findViewById(R.id.button_start); - mStopButton = (Button) findViewById(R.id.button_stop); - mShareButton = (Button) findViewById(R.id.button_share); - mShareButton.setEnabled(false); - mFailButton = (Button) findViewById(R.id.button_fail); - mSkipButton = (Button) findViewById(R.id.button_skip); - updateStartStopButtons(false); - updateFailSkipButton(false); - } - - private void updateStartStopButtons(boolean running) { - mStartButton.setEnabled(!running); - mStopButton.setEnabled(running); - } - - @Override - protected void onStart() { - super.onStart(); - setActivityType(ACTIVITY_TEST_DISCONNECT); - } - - @Override - boolean isOutput() { - return true; - } - - @Override - public void setupEffects(int sessionId) { - } - - private void updateFailSkipButton(final boolean running) { - runOnUiThread(new Runnable() { - @Override - public void run() { - mFailButton.setEnabled(running); - mSkipButton.setEnabled(running); - } - }); - } - - // Write to scrollable TextView - private void log(final String text) { - runOnUiThread(new Runnable() { - @Override - public void run() { - mAutoTextView.append(text); - mAutoTextView.append("\n"); - } - }); - } - - // Write to status and command view - private void setInstructionsText(final String text) { - runOnUiThread(new Runnable() { - @Override - public void run() { - mInstructionsTextView.setText(text); - } - }); - } - - // Write to status and command view - private void setStatusText(final String text) { - runOnUiThread(new Runnable() { - @Override - public void run() { - mStatusTextView.setText(text); - } - }); - } - - private void logClear() { - runOnUiThread(new Runnable() { - @Override - public void run() { - mAutoTextView.setText(""); - } - }); - } - - @Override - public void onResume() { - super.onResume(); - IntentFilter filter = new IntentFilter(Intent.ACTION_HEADSET_PLUG); - this.registerReceiver(mPluginReceiver, filter); - } - - @Override - public void onPause() { - this.unregisterReceiver(mPluginReceiver); - super.onPause(); - } - - // Only call from UI thread. - public void onTestFinished() { - updateStartStopButtons(false); - mShareButton.setEnabled(true); - } - - public void startAudioTest() throws IOException { - openAudio(); - startAudio(); - } - - public void stopAudioTest() { - stopAudioQuiet(); - closeAudio(); - } - - public void onCancel(View view) { - stopAudioTest(); - onTestFinished(); - } - - // Called on UI thread - public void onStopAudioTest(View view) { - stopAudioTest(); - onTestFinished(); - keepScreenOn(false); - } - - public void onStartDisconnectTest(View view) { - updateStartStopButtons(true); - mThreadEnabled = true; - mAutoThread = new Thread(this); - mAutoThread.start(); - } - - public void onStopDisconnectTest(View view) { - try { - if (mAutoThread != null) { - mThreadEnabled = false; - mAutoThread.interrupt(); - mAutoThread.join(100); - mAutoThread = null; - } - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - - public void onFailTest(View view) { - mTestFailed = true; - } - - public void onSkipTest(View view) { - mSkipTest = true; - } - - // Share text from log via GMail, Drive or other method. - public void onShareResult(View view) { - Intent sharingIntent = new Intent(Intent.ACTION_SEND); - sharingIntent.setType("text/plain"); - - String subjectText = "OboeTester Test Disconnect result " + getTimestampString(); - sharingIntent.putExtra(Intent.EXTRA_SUBJECT, subjectText); - - String shareBody = mAutoTextView.getText().toString(); - sharingIntent.putExtra(Intent.EXTRA_TEXT, shareBody); - - startActivity(Intent.createChooser(sharingIntent, "Share using:")); - } - - private String getConfigText(StreamConfiguration config) { - return ((config.getDirection() == StreamConfiguration.DIRECTION_OUTPUT) ? "OUT" : "IN") - + ", Perf = " + StreamConfiguration.convertPerformanceModeToText( - config.getPerformanceMode()) - + ", " + StreamConfiguration.convertSharingModeToText(config.getSharingMode()); - } - - private void testConfiguration(boolean isInput, - int perfMode, - int sharingMode, - int channelCount, - boolean requestPlugin) throws InterruptedException { - String actualConfigText = "none"; - mSkipTest = false; - - AudioInputTester mAudioInTester = null; - AudioOutputTester mAudioOutTester = null; - - clearStreamContexts(); - - if (isInput) { - mAudioInTester = addAudioInputTester(); - } else { - mAudioOutTester = addAudioOutputTester(); - } - - // Configure settings - StreamConfiguration requestedConfig = (isInput) - ? mAudioInTester.requestedConfiguration - : mAudioOutTester.requestedConfiguration; - StreamConfiguration actualConfig = (isInput) - ? mAudioInTester.actualConfiguration - : mAudioOutTester.actualConfiguration; - - requestedConfig.reset(); - requestedConfig.setPerformanceMode(perfMode); - requestedConfig.setSharingMode(sharingMode); - requestedConfig.setChannelCount(channelCount); - - log("========================== #" + mTestCount); - log("Requested:"); - log(getConfigText(requestedConfig)); - - // Give previous stream time to close and release resources. Avoid race conditions. - Thread.sleep(SETTLING_TIME_MILLIS); - if (!mThreadEnabled) return; - boolean openFailed = false; - AudioStreamBase stream = null; - try { - startAudioTest(); // this will fill in actualConfig - log("Actual:"); - actualConfigText = getConfigText(actualConfig) - + ", " + (actualConfig.isMMap() ? "MMAP" : "Legacy"); - log(actualConfigText); - - stream = (isInput) - ? mAudioInTester.getCurrentAudioStream() - : mAudioOutTester.getCurrentAudioStream(); - } catch (IOException e) { - openFailed = true; - log(e.getMessage()); - } - - // The test is only worth running if we got the configuration we requested. - boolean valid = true; - if (!openFailed) { - if(actualConfig.getSharingMode() != sharingMode) { - log("did not get requested sharing mode"); - valid = false; - } - if (actualConfig.getPerformanceMode() != perfMode) { - log("did not get requested performance mode"); - valid = false; - } - if (actualConfig.getNativeApi() == StreamConfiguration.NATIVE_API_OPENSLES) { - log("OpenSL ES does not support automatic disconnect"); - valid = false; - } - } - - int oldPlugCount = mPlugCount; - if (!openFailed && valid) { - mTestFailed = false; - updateFailSkipButton(true); - // poll for stream disconnected - while (!mTestFailed && mThreadEnabled && !mSkipTest && - stream.getState() == StreamConfiguration.STREAM_STATE_STARTING) { - Thread.sleep(POLL_DURATION_MILLIS); - } - String message = (requestPlugin ? "Plug IN" : "UNplug") + " headset now!"; - setStatusText("Testing:\n" + actualConfigText); - setInstructionsText(message); - int timeoutCount = 0; - // Wait for Java plug count to change or stream to disconnect. - while (!mTestFailed && mThreadEnabled && !mSkipTest && - stream.getState() == StreamConfiguration.STREAM_STATE_STARTED) { - Thread.sleep(POLL_DURATION_MILLIS); - if (mPlugCount > oldPlugCount) { - timeoutCount = TIME_TO_FAILURE_MILLIS / POLL_DURATION_MILLIS; - break; - } - } - // Wait for timeout or stream to disconnect. - while (!mTestFailed && mThreadEnabled && !mSkipTest && (timeoutCount > 0) && - stream.getState() == StreamConfiguration.STREAM_STATE_STARTED) { - Thread.sleep(POLL_DURATION_MILLIS); - timeoutCount--; - if (timeoutCount == 0) { - mTestFailed = true; - } else { - setStatusText("Plug detected by Java.\nCounting down to Oboe failure: " + timeoutCount); - } - } - setStatusText(mTestFailed ? "Failed" : "Passed - detected"); - } - updateFailSkipButton(false); - setInstructionsText("Wait..."); - - if (!openFailed) { - stopAudioTest(); - } - - if (mSkipTest) valid = false; - - if (valid) { - if (openFailed) { - mFailedSummary.append("------ #" + mTestCount); - mFailedSummary.append("\n"); - mFailedSummary.append(getConfigText(requestedConfig)); - mFailedSummary.append("\n"); - mFailedSummary.append("Open failed!\n"); - mFailCount++; - } else { - log("Result:"); - boolean passed = !mTestFailed; - String resultText = requestPlugin ? "plugIN" : "UNplug"; - resultText += ", " + (passed ? TEXT_PASS : TEXT_FAIL); - log(resultText); - if (!passed) { - mFailedSummary.append("------ #" + mTestCount); - mFailedSummary.append("\n"); - mFailedSummary.append(" "); - mFailedSummary.append(actualConfigText); - mFailedSummary.append("\n"); - mFailedSummary.append(" "); - mFailedSummary.append(resultText); - mFailedSummary.append("\n"); - mFailCount++; - } else { - mPassCount++; - } - } - } else { - log(TEXT_SKIP); - } - // Give hardware time to settle between tests. - Thread.sleep(1000); - mTestCount++; - } - - private void testConfiguration(boolean isInput, int performanceMode, - int sharingMode) throws InterruptedException { - int channelCount = 2; - boolean requestPlugin = true; // plug IN - testConfiguration(isInput, performanceMode, sharingMode, channelCount, requestPlugin); - requestPlugin = false; // UNplug - testConfiguration(isInput, performanceMode, sharingMode, channelCount, requestPlugin); - } - - private void testConfiguration(int performanceMode, - int sharingMode) throws InterruptedException { - testConfiguration(false, performanceMode, sharingMode); - testConfiguration(true, performanceMode, sharingMode); - } - - @Override - public void run() { - mPlugCount = 0; - logClear(); - log("=== STARTED at " + new Date()); - log(Build.MANUFACTURER + " " + Build.PRODUCT); - log(Build.DISPLAY); - mFailedSummary = new StringBuffer(); - mTestCount = 0; - mPassCount = 0; - mFailCount = 0; - // Try several different configurations. - try { - testConfiguration(StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY, - StreamConfiguration.SHARING_MODE_EXCLUSIVE); - testConfiguration(StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY, - StreamConfiguration.SHARING_MODE_SHARED); - testConfiguration(StreamConfiguration.PERFORMANCE_MODE_NONE, - StreamConfiguration.SHARING_MODE_SHARED); - } catch (InterruptedException e) { - e.printStackTrace(); - } finally { - stopAudioTest(); - setInstructionsText("See summary below."); - setStatusText("Finished."); - log("\n==== SUMMARY ========"); - if (mFailCount > 0) { - log(mPassCount + " passed. " + mFailCount + " failed."); - log("These tests FAILED:"); - log(mFailedSummary.toString()); - } else { - log("All tests PASSED."); - } - log("== FINISHED at " + new Date()); - runOnUiThread(new Runnable() { - @Override - public void run() { - onTestFinished(); - } - }); - updateFailSkipButton(false); - } - } - -} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestOutputActivity.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestOutputActivity.java deleted file mode 100644 index d7066efc..00000000 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestOutputActivity.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright 2017 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. - */ - -package com.google.sample.oboe.manualtest; - -import android.os.Bundle; -import android.util.Log; -import android.view.View; -import android.widget.AdapterView; -import android.widget.CheckBox; -import android.widget.Spinner; - -import java.io.IOException; - -/** - * Test basic output. - */ -public final class TestOutputActivity extends TestOutputActivityBase { - - public static final int MAX_CHANNEL_BOXES = 8; - private CheckBox[] mChannelBoxes; - private Spinner mNativeApiSpinner; - - private class NativeApiSpinnerListener implements android.widget.AdapterView.OnItemSelectedListener { - @Override - public void onItemSelected(AdapterView parent, View view, int pos, long id) { - mAudioOutTester.setSignalType(pos); - } - - @Override - public void onNothingSelected(AdapterView parent) { - mAudioOutTester.setSignalType(0); - } - } - - @Override - protected void inflateActivity() { - setContentView(R.layout.activity_test_output); - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - updateEnabledWidgets(); - - mAudioOutTester = addAudioOutputTester(); - - mChannelBoxes = new CheckBox[MAX_CHANNEL_BOXES]; - int ic = 0; - mChannelBoxes[ic++] = (CheckBox) findViewById(R.id.channelBox0); - mChannelBoxes[ic++] = (CheckBox) findViewById(R.id.channelBox1); - mChannelBoxes[ic++] = (CheckBox) findViewById(R.id.channelBox2); - mChannelBoxes[ic++] = (CheckBox) findViewById(R.id.channelBox3); - mChannelBoxes[ic++] = (CheckBox) findViewById(R.id.channelBox4); - mChannelBoxes[ic++] = (CheckBox) findViewById(R.id.channelBox5); - mChannelBoxes[ic++] = (CheckBox) findViewById(R.id.channelBox6); - mChannelBoxes[ic++] = (CheckBox) findViewById(R.id.channelBox7); - configureChannelBoxes(0); - - - mNativeApiSpinner = (Spinner) findViewById(R.id.spinnerOutputSignal); - mNativeApiSpinner.setOnItemSelectedListener(new NativeApiSpinnerListener()); - mNativeApiSpinner.setSelection(StreamConfiguration.NATIVE_API_UNSPECIFIED); - } - - @Override - protected void onStart() { - super.onStart(); - setActivityType(ACTIVITY_TEST_OUTPUT); - } - - public void openAudio() throws IOException { - super.openAudio(); - int channelCount = mAudioOutTester.getCurrentAudioStream().getChannelCount(); - configureChannelBoxes(channelCount); - } - - private void configureChannelBoxes(int channelCount) { - for (int i = 0; i < mChannelBoxes.length; i++) { - mChannelBoxes[i].setChecked(i < channelCount); - mChannelBoxes[i].setEnabled(i < channelCount); - } - } - - public void startAudio() { - super.startAudio(); - mAudioOutTester.setToneType(OboeAudioOutputStream.TONE_TYPE_SINE); - mAudioOutTester.setEnabled(true); - } - - public void stopAudio() { - mAudioOutTester.setEnabled(false); - super.stopAudio(); - } - - public void closeAudio() { - configureChannelBoxes(0); - super.closeAudio(); - } - - public void onChannelBoxClicked(View view) { - CheckBox checkBox = (CheckBox) view; - String text = (String) checkBox.getText(); - int channelIndex = Integer.parseInt(text); - mAudioOutTester.setChannelEnabled(channelIndex, checkBox.isChecked()); - } -} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/audio_device/AudioDeviceAdapter.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/audio_device/AudioDeviceAdapter.java similarity index 91% rename from mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/audio_device/AudioDeviceAdapter.java rename to mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/audio_device/AudioDeviceAdapter.java index 9fa00939..4542227b 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/audio_device/AudioDeviceAdapter.java +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/audio_device/AudioDeviceAdapter.java @@ -1,4 +1,4 @@ -package com.google.sample.audio_device; +package com.mobileer.audio_device; /* * Copyright 2017 The Android Open Source Project * @@ -16,15 +16,16 @@ package com.google.sample.audio_device; */ import android.content.Context; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.TextView; -import com.google.sample.oboe.manualtest.R; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.mobileer.oboetester.R; /** * Provides views for a list of audio devices. Usually used as an Adapter for a Spinner or ListView. diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/audio_device/AudioDeviceInfoConverter.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/audio_device/AudioDeviceInfoConverter.java similarity index 56% rename from mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/audio_device/AudioDeviceInfoConverter.java rename to mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/audio_device/AudioDeviceInfoConverter.java index 5d053955..14c635d2 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/audio_device/AudioDeviceInfoConverter.java +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/audio_device/AudioDeviceInfoConverter.java @@ -1,4 +1,4 @@ -package com.google.sample.audio_device; +package com.mobileer.audio_device; /* * Copyright 2017 The Android Open Source Project * @@ -15,9 +15,15 @@ package com.google.sample.audio_device; * limitations under the License. */ +import android.media.AudioDescriptor; import android.media.AudioDeviceInfo; +import android.media.AudioProfile; +import android.os.Build; -class AudioDeviceInfoConverter { +import java.util.List; +import java.util.Locale; + +public class AudioDeviceInfoConverter { /** * Converts an {@link AudioDeviceInfo} object into a human readable representation @@ -25,7 +31,7 @@ class AudioDeviceInfoConverter { * @param adi The AudioDeviceInfo object to be converted to a String * @return String containing all the information from the AudioDeviceInfo object */ - static String toString(AudioDeviceInfo adi){ + public static String toString(AudioDeviceInfo adi){ StringBuilder sb = new StringBuilder(); sb.append("Id: "); @@ -49,11 +55,11 @@ class AudioDeviceInfoConverter { sb.append("\nChannel masks: "); int[] channelMasks = adi.getChannelMasks(); - sb.append(intArrayToString(channelMasks)); + sb.append(intArrayToStringHex(channelMasks)); sb.append("\nChannel index masks: "); int[] channelIndexMasks = adi.getChannelIndexMasks(); - sb.append(intArrayToString(channelIndexMasks)); + sb.append(intArrayToStringHex(channelIndexMasks)); sb.append("\nEncodings: "); int[] encodings = adi.getEncodings(); @@ -63,6 +69,36 @@ class AudioDeviceInfoConverter { int[] sampleRates = adi.getSampleRates(); sb.append(intArrayToString(sampleRates)); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + sb.append("\nAddress: "); + sb.append(adi.getAddress()); + } + + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { + sb.append("\nEncapsulation Metadata Types: "); + int[] encapsulationMetadataTypes = adi.getEncapsulationMetadataTypes(); + sb.append(intArrayToString(encapsulationMetadataTypes)); + } + + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { + sb.append("\nEncapsulation Modes: "); + int[] encapsulationModes = adi.getEncapsulationModes(); + sb.append(intArrayToString(encapsulationModes)); + } + + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { + sb.append("\nAudio Descriptors: "); + List audioDescriptors = adi.getAudioDescriptors(); + sb.append(audioDescriptors); + } + + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { + sb.append("\nAudio Profiles: "); + List audioProfiles = adi.getAudioProfiles(); + sb.append(audioProfiles); + } + + sb.append("\n"); return sb.toString(); } @@ -76,7 +112,22 @@ class AudioDeviceInfoConverter { StringBuilder sb = new StringBuilder(); for (int i = 0; i < integerArray.length; i++){ sb.append(integerArray[i]); - if (i != integerArray.length -1) sb.append(" "); + if (i != integerArray.length - 1) sb.append(" "); + } + return sb.toString(); + } + + /** + * Converts an integer array into a hexadecimal string where each int is separated by a space + * + * @param integerArray the integer array to convert to a string + * @return string containing all the integer values separated by spaces + */ + private static String intArrayToStringHex(int[] integerArray){ + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < integerArray.length; i++){ + sb.append(String.format(Locale.getDefault(), "0x%02X", integerArray[i])); + if (i != integerArray.length - 1) sb.append(" "); } return sb.toString(); } @@ -88,38 +139,56 @@ class AudioDeviceInfoConverter { * e.g. AudioDeviceInfo.TYPE_BUILT_IN_SPEAKER * @return string which describes the type of audio device */ - static String typeToString(int type){ + public static String typeToString(int type){ switch (type) { case AudioDeviceInfo.TYPE_AUX_LINE: return "auxiliary line-level connectors"; + case AudioDeviceInfo.TYPE_BLE_BROADCAST: + return "BLE broadcast"; + case AudioDeviceInfo.TYPE_BLE_HEADSET: + return "BLE headset"; + case AudioDeviceInfo.TYPE_BLE_SPEAKER: + return "BLE speaker"; case AudioDeviceInfo.TYPE_BLUETOOTH_A2DP: - return "Bluetooth device supporting the A2DP profile"; + return "Bluetooth A2DP"; case AudioDeviceInfo.TYPE_BLUETOOTH_SCO: - return "Bluetooth device typically used for telephony"; + return "Bluetooth telephony SCO"; case AudioDeviceInfo.TYPE_BUILTIN_EARPIECE: - return "built-in earphone speaker"; + return "built-in earpiece"; case AudioDeviceInfo.TYPE_BUILTIN_MIC: return "built-in microphone"; case AudioDeviceInfo.TYPE_BUILTIN_SPEAKER: return "built-in speaker"; + case AudioDeviceInfo.TYPE_BUILTIN_SPEAKER_SAFE: + return "built-in speaker safe"; case AudioDeviceInfo.TYPE_BUS: - return "BUS"; + return "bus"; case AudioDeviceInfo.TYPE_DOCK: - return "DOCK"; + return "dock"; + case 31: // AudioDeviceInfo.TYPE_DOCK_ANALOG: + return "dock analog"; + case 28: // AudioDeviceInfo.TYPE_ECHO_REFERENCE: + return "echo reference"; case AudioDeviceInfo.TYPE_FM: return "FM"; case AudioDeviceInfo.TYPE_FM_TUNER: return "FM tuner"; case AudioDeviceInfo.TYPE_HDMI: return "HDMI"; + case AudioDeviceInfo.TYPE_HEARING_AID: + return "hearing aid"; case AudioDeviceInfo.TYPE_HDMI_ARC: return "HDMI audio return channel"; + case AudioDeviceInfo.TYPE_HDMI_EARC: + return "HDMI enhanced ARC"; case AudioDeviceInfo.TYPE_IP: return "IP"; case AudioDeviceInfo.TYPE_LINE_ANALOG: return "line analog"; case AudioDeviceInfo.TYPE_LINE_DIGITAL: return "line digital"; + case AudioDeviceInfo.TYPE_REMOTE_SUBMIX: + return "remote submix"; case AudioDeviceInfo.TYPE_TELEPHONY: return "telephony"; case AudioDeviceInfo.TYPE_TV_TUNER: @@ -128,13 +197,15 @@ class AudioDeviceInfoConverter { return "USB accessory"; case AudioDeviceInfo.TYPE_USB_DEVICE: return "USB device"; + case AudioDeviceInfo.TYPE_USB_HEADSET: + return "USB headset"; case AudioDeviceInfo.TYPE_WIRED_HEADPHONES: return "wired headphones"; case AudioDeviceInfo.TYPE_WIRED_HEADSET: return "wired headset"; default: case AudioDeviceInfo.TYPE_UNKNOWN: - return "unknown"; + return "unknown=" + type; } } } diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/audio_device/AudioDeviceListEntry.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/audio_device/AudioDeviceListEntry.java similarity index 85% rename from mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/audio_device/AudioDeviceListEntry.java rename to mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/audio_device/AudioDeviceListEntry.java index 15f3e23b..baceb475 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/audio_device/AudioDeviceListEntry.java +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/audio_device/AudioDeviceListEntry.java @@ -1,4 +1,4 @@ -package com.google.sample.audio_device; +package com.mobileer.audio_device; /* * Copyright 2017 The Android Open Source Project * @@ -32,9 +32,16 @@ public class AudioDeviceListEntry { private int mId; private String mName; - public AudioDeviceListEntry(int deviceId, String deviceName){ + private AudioDeviceInfo mDeviceInfo; + + public AudioDeviceListEntry(int deviceId, String deviceName) { + this(deviceId, deviceName, null); + } + + public AudioDeviceListEntry(int deviceId, String deviceName, AudioDeviceInfo deviceInfo) { mId = deviceId; mName = deviceName; + mDeviceInfo = deviceInfo; } public int getId() { @@ -45,6 +52,8 @@ public class AudioDeviceListEntry { return mName; } + public AudioDeviceInfo getDeviceInfo() { return mDeviceInfo; } + public String toString(){ return getName(); } @@ -84,8 +93,11 @@ public class AudioDeviceListEntry { if (directionType == AudioManager.GET_DEVICES_ALL || (directionType == AudioManager.GET_DEVICES_OUTPUTS && info.isSink()) || (directionType == AudioManager.GET_DEVICES_INPUTS && info.isSource())) { - listEntries.add(new AudioDeviceListEntry(info.getId(), info.getProductName() + " " + - AudioDeviceInfoConverter.typeToString(info.getType()))); + listEntries.add(new AudioDeviceListEntry(info.getId(), + info.getId() + ": " + + info.getProductName() + " " + + AudioDeviceInfoConverter.typeToString(info.getType()), + info)); } } return listEntries; diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/audio_device/AudioDeviceSpinner.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/audio_device/AudioDeviceSpinner.java similarity index 87% rename from mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/audio_device/AudioDeviceSpinner.java rename to mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/audio_device/AudioDeviceSpinner.java index 3dcdc082..2e0333d7 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/audio_device/AudioDeviceSpinner.java +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/audio_device/AudioDeviceSpinner.java @@ -1,4 +1,4 @@ -package com.google.sample.audio_device; +package com.mobileer.audio_device; /* * Copyright 2017 The Android Open Source Project * @@ -22,13 +22,14 @@ import android.media.AudioDeviceCallback; import android.media.AudioDeviceInfo; import android.media.AudioManager; import android.util.AttributeSet; -import android.widget.Spinner; -import com.google.sample.oboe.manualtest.R; +import androidx.appcompat.widget.AppCompatSpinner; + +import com.mobileer.oboetester.R; import java.util.List; -public class AudioDeviceSpinner extends Spinner { +public class AudioDeviceSpinner extends AppCompatSpinner { private static final int AUTO_SELECT_DEVICE_ID = 0; private static final String TAG = AudioDeviceSpinner.class.getName(); @@ -63,14 +64,8 @@ public class AudioDeviceSpinner extends Spinner { } public AudioDeviceSpinner(Context context, AttributeSet attrs, int defStyleAttr, - int defStyleRes, int mode){ - super(context, attrs, defStyleAttr, defStyleRes, mode); - setup(context); - } - - public AudioDeviceSpinner(Context context, AttributeSet attrs, int defStyleAttr, - int defStyleRes, int mode, Theme popupTheme){ - super(context, attrs, defStyleAttr, defStyleRes, mode, popupTheme); + int mode, Theme popupTheme){ + super(context, attrs, defStyleAttr, mode, popupTheme); setup(context); } diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/audio_device/CommunicationDeviceSpinner.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/audio_device/CommunicationDeviceSpinner.java new file mode 100644 index 00000000..32ccda5d --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/audio_device/CommunicationDeviceSpinner.java @@ -0,0 +1,126 @@ +package com.mobileer.audio_device; +/* + * 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. + */ + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.Resources.Theme; +import android.media.AudioDeviceCallback; +import android.media.AudioDeviceInfo; +import android.media.AudioManager; +import android.util.AttributeSet; + +import androidx.appcompat.widget.AppCompatSpinner; + +import com.mobileer.oboetester.R; + +import java.util.List; + +public class CommunicationDeviceSpinner extends AppCompatSpinner { + private static final String TAG = CommunicationDeviceSpinner.class.getName(); + // menu positions + public static final int POS_CLEAR = 0; + public static final int POS_DEVICES = 1; // base position for device list + private AudioDeviceAdapter mDeviceAdapter; + private AudioManager mAudioManager; + private Context mContext; + AudioDeviceInfo[] mCommDeviceArray = null; + + public CommunicationDeviceSpinner(Context context){ + super(context); + setup(context); + } + + public CommunicationDeviceSpinner(Context context, int mode){ + super(context, mode); + setup(context); + } + + public CommunicationDeviceSpinner(Context context, AttributeSet attrs){ + super(context, attrs); + setup(context); + } + + public CommunicationDeviceSpinner(Context context, AttributeSet attrs, int defStyleAttr){ + super(context, attrs, defStyleAttr); + setup(context); + } + + public CommunicationDeviceSpinner(Context context, AttributeSet attrs, int defStyleAttr, int mode){ + super(context, attrs, defStyleAttr, mode); + setup(context); + } + + public CommunicationDeviceSpinner(Context context, AttributeSet attrs, int defStyleAttr, + int mode, Theme popupTheme){ + super(context, attrs, defStyleAttr, mode, popupTheme); + setup(context); + } + + public AudioDeviceInfo[] getCommunicationsDevices() { + return mCommDeviceArray; + } + + private void setup(Context context){ + mContext = context; + + mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + + mDeviceAdapter = new AudioDeviceAdapter(context); + setAdapter(mDeviceAdapter); + + // Add default entries to the list and select one. + addDefaultDevicesOptions(); + setSelection(POS_CLEAR); + setupCommunicationDeviceListener(); + } + + @TargetApi(31) + private void setupCommunicationDeviceListener(){ + // Note that we will immediately receive a call to onDevicesAdded with the list of + // devices which are currently connected. + mAudioManager.registerAudioDeviceCallback(new AudioDeviceCallback() { + @Override + public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) { + updateDeviceList(); + } + + public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) { + updateDeviceList(); + } + + private void updateDeviceList() { + mDeviceAdapter.clear(); + addDefaultDevicesOptions(); + setSelection(POS_CLEAR); + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { + List commDeviceList = mAudioManager.getAvailableCommunicationDevices(); + mCommDeviceArray = commDeviceList.toArray(new AudioDeviceInfo[0]); + // Communications Devices are always OUTPUTS. + List deviceList = + AudioDeviceListEntry.createListFrom( + mCommDeviceArray, AudioManager.GET_DEVICES_OUTPUTS); + mDeviceAdapter.addAll(deviceList); + } + } + }, null); + } + + private void addDefaultDevicesOptions() { + mDeviceAdapter.add(new AudioDeviceListEntry(POS_CLEAR, + mContext.getString(R.string.clear_comm))); + } +} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/miditools/synth/SynthEngine.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/miditools/synth/SynthEngine.java index 1e1c3a8b..da5d7512 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/miditools/synth/SynthEngine.java +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/miditools/synth/SynthEngine.java @@ -27,6 +27,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Hashtable; import java.util.Iterator; +import java.util.Locale; /** * Very simple polyphonic, single channel synthesizer. It runs a background @@ -178,7 +179,7 @@ public class SynthEngine extends MidiReceiver { public void logMidiMessage(byte[] data, int offset, int count) { String text = "Received: "; for (int i = 0; i < count; i++) { - text += String.format("0x%02X, ", data[offset + i]); + text += String.format(Locale.getDefault(), "0x%02X, ", data[offset + i]); } Log.i(TAG, text); } diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AnalyzerActivity.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AnalyzerActivity.java similarity index 51% rename from mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AnalyzerActivity.java rename to mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AnalyzerActivity.java index b035881a..69a8b829 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AnalyzerActivity.java +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AnalyzerActivity.java @@ -14,43 +14,27 @@ * limitations under the License. */ -package com.google.sample.oboe.manualtest; +package com.mobileer.oboetester; -import android.Manifest; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.os.Build; import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; -import android.support.annotation.NonNull; -import android.support.v4.app.ActivityCompat; -import android.support.v4.content.ContextCompat; -import android.view.View; -import android.widget.Button; -import android.widget.TextView; import android.widget.Toast; -import java.io.File; -import java.io.FileOutputStream; +import androidx.annotation.NonNull; + import java.io.IOException; -import java.io.OutputStreamWriter; -import java.io.Writer; +import java.util.Locale; /** * Activity to measure latency on a full duplex stream. */ public class AnalyzerActivity extends TestInputActivity { - private static final int MY_PERMISSIONS_REQUEST_EXTERNAL_STORAGE = 1001; - - protected static final String KEY_FILE_NAME = "file"; - protected static final String KEY_BUFFER_BURSTS = "buffer_bursts"; - AudioOutputTester mAudioOutTester; protected BufferSizeView mBufferSizeView; - protected String mResultFileName; - private String mTestResults; + protected AutomatedTestRunner mAutomatedTestRunner; // Note that these string must match the enum result_code in LatencyAnalyzer.h String resultCodeToString(int resultCode) { @@ -81,6 +65,7 @@ public class AnalyzerActivity extends TestInputActivity { public native int getMeasuredResult(); public native int getResetCount(); + @Override @NonNull protected String getCommonTestReport() { StringBuffer report = new StringBuffer(); @@ -88,8 +73,8 @@ public class AnalyzerActivity extends TestInputActivity { report.append("build.fingerprint = " + Build.FINGERPRINT + "\n"); try { PackageInfo pinfo = getPackageManager().getPackageInfo(getPackageName(), 0); - report.append(String.format("test.version = %s\n", pinfo.versionName)); - report.append(String.format("test.version.code = %d\n", pinfo.versionCode)); + report.append(String.format(Locale.getDefault(), "test.version = %s\n", pinfo.versionName)); + report.append(String.format(Locale.getDefault(), "test.version.code = %d\n", pinfo.versionCode)); } catch (PackageManager.NameNotFoundException e) { } report.append("time.millis = " + System.currentTimeMillis() + "\n"); @@ -97,18 +82,18 @@ public class AnalyzerActivity extends TestInputActivity { // INPUT report.append(mAudioInputTester.actualConfiguration.dump()); AudioStreamBase inStream = mAudioInputTester.getCurrentAudioStream(); - report.append(String.format("in.burst.frames = %d\n", inStream.getFramesPerBurst())); - report.append(String.format("in.xruns = %d\n", inStream.getXRunCount())); + report.append(String.format(Locale.getDefault(), "in.burst.frames = %d\n", inStream.getFramesPerBurst())); + report.append(String.format(Locale.getDefault(), "in.xruns = %d\n", inStream.getXRunCount())); // OUTPUT report.append(mAudioOutTester.actualConfiguration.dump()); AudioStreamBase outStream = mAudioOutTester.getCurrentAudioStream(); - report.append(String.format("out.burst.frames = %d\n", outStream.getFramesPerBurst())); + report.append(String.format(Locale.getDefault(), "out.burst.frames = %d\n", outStream.getFramesPerBurst())); int bufferSize = outStream.getBufferSizeInFrames(); - report.append(String.format("out.buffer.size.frames = %d\n", bufferSize)); + report.append(String.format(Locale.getDefault(), "out.buffer.size.frames = %d\n", bufferSize)); int bufferCapacity = outStream.getBufferCapacityInFrames(); - report.append(String.format("out.buffer.capacity.frames = %d\n", bufferCapacity)); - report.append(String.format("out.xruns = %d\n", outStream.getXRunCount())); + report.append(String.format(Locale.getDefault(), "out.buffer.capacity.frames = %d\n", bufferCapacity)); + report.append(String.format(Locale.getDefault(), "out.xruns = %d\n", outStream.getXRunCount())); return report.toString(); } @@ -118,9 +103,6 @@ public class AnalyzerActivity extends TestInputActivity { super.onCreate(savedInstanceState); mAudioOutTester = addAudioOutputTester(); mBufferSizeView = (BufferSizeView) findViewById(R.id.buffer_size_view); - if (mBufferSizeView != null) { - mBufferSizeView.setAudioOutTester(mAudioOutTester); - } } @Override @@ -144,11 +126,12 @@ public class AnalyzerActivity extends TestInputActivity { } } - public void startAudio() { - if (mBufferSizeView != null && mBufferSizeView.isEnabled()) { - mBufferSizeView.updateBufferSize(); + @Override + public void openAudio() throws IOException { + super.openAudio(); + if (mBufferSizeView != null) { + mBufferSizeView.onStreamOpened((OboeAudioStream) mAudioOutTester.getCurrentAudioStream()); } - super.startAudio(); } public void onStreamClosed() { @@ -162,75 +145,14 @@ public class AnalyzerActivity extends TestInputActivity { public void stopAudioTest() { } - void writeTestResultIfPermitted(String resultString) { - // Here, thisActivity is the current activity - if (ContextCompat.checkSelfPermission(this, - Manifest.permission.WRITE_EXTERNAL_STORAGE) - != PackageManager.PERMISSION_GRANTED) { - mTestResults = resultString; - ActivityCompat.requestPermissions(this, - new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, - MY_PERMISSIONS_REQUEST_EXTERNAL_STORAGE); - } else { - // Permission has already been granted - writeTestResult(resultString); - } - } - - void maybeWriteTestResult(String resultString) { - if (mResultFileName == null) return; - writeTestResultIfPermitted(resultString); - } - @Override - public void onRequestPermissionsResult(int requestCode, - String[] permissions, - int[] grantResults) { - switch (requestCode) { - case MY_PERMISSIONS_REQUEST_EXTERNAL_STORAGE: { - // If request is cancelled, the result arrays are empty. - if (grantResults.length > 0 - && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - writeTestResult(mTestResults); - } else { - showToast("Writing external storage needed for test results."); - } - return; - } + public void saveIntentLog() { + if (mTestRunningByIntent) { + String report = mAutomatedTestRunner.getFullLogs(); + maybeWriteTestResult(report); + mTestRunningByIntent = false; + mBundleFromIntent = null; } } - private void writeTestInBackground(final String resultString) { - new Thread() { - public void run() { - writeTestResult(resultString); - } - }.start(); - } - - // Run this in a background thread. - private void writeTestResult(String resultString) { - File resultFile = new File(mResultFileName); - Writer writer = null; - try { - writer = new OutputStreamWriter(new FileOutputStream(resultFile)); - writer.write(resultString); - } catch ( - IOException e) { - e.printStackTrace(); - showErrorToast(" writing result file. " + e.getMessage()); - } finally { - if (writer != null) { - try { - writer.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - } - - mResultFileName = null; - } - - } diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioForegroundService.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioForegroundService.java new file mode 100644 index 00000000..2ebb5a2f --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioForegroundService.java @@ -0,0 +1,80 @@ +/* + * Copyright 2024 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. + */ + +package com.mobileer.oboetester; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ServiceInfo; +import android.os.Build; +import android.os.IBinder; +import android.util.Log; + +public class AudioForegroundService extends Service { + private static final String TAG = "OboeTester"; + public static final String ACTION_START = "ACTION_START"; + public static final String ACTION_STOP = "ACTION_STOP"; + + @Override + public IBinder onBind(Intent intent) { + // We don't provide binding, so return null + return null; + } + + private Notification buildNotification() { + NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + manager.createNotificationChannel(new NotificationChannel( + "all", + "All Notifications", + NotificationManager.IMPORTANCE_NONE)); + + return new Notification.Builder(this, "all") + .setContentTitle("Playing/recording audio") + .setContentText("playing/recording...") + .setSmallIcon(R.drawable.ic_notification) + .build(); + } + return null; + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + Log.i(TAG, "Receive onStartCommand" + intent); + switch (intent.getAction()) { + case ACTION_START: + Log.i(TAG, "Receive ACTION_START " + intent.getExtras()); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + int serviceTypes = intent.getIntExtra("service_types", 0); + Log.i(TAG, "ServiceTypes: " + serviceTypes); + startForeground(1, buildNotification(), serviceTypes); + } + break; + case ACTION_STOP: + Log.i(TAG, "Receive ACTION_STOP " + intent.getExtras()); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + stopForeground(STOP_FOREGROUND_REMOVE); + } + break; + } + return START_NOT_STICKY; + } +} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AudioInputTester.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioInputTester.java similarity index 96% rename from mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AudioInputTester.java rename to mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioInputTester.java index 81c9cd67..a03824bf 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AudioInputTester.java +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioInputTester.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.sample.oboe.manualtest; +package com.mobileer.oboetester; import android.util.Log; diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AudioOutputTester.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioOutputTester.java similarity index 80% rename from mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AudioOutputTester.java rename to mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioOutputTester.java index 8185b48f..04c3f2e3 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AudioOutputTester.java +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioOutputTester.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.sample.oboe.manualtest; +package com.mobileer.oboetester; import android.util.Log; @@ -36,18 +36,11 @@ public class AudioOutputTester extends AudioStreamTester { Log.i(TapToToneActivity.TAG, "create OboeAudioOutputStream ---------"); mOboeAudioOutputStream = new OboeAudioOutputStream(); mCurrentAudioStream = mOboeAudioOutputStream; - setToneType(OboeAudioOutputStream.TONE_TYPE_SINE); - setEnabled(false); requestedConfiguration.setDirection(StreamConfiguration.DIRECTION_OUTPUT); } - public void setToneType(int index) { - Log.i(TapToToneActivity.TAG, "setToneType(" + index + ")"); - mOboeAudioOutputStream.setToneType(index); - } - - public void setEnabled(boolean flag) { - mOboeAudioOutputStream.setToneEnabled(flag); + public void trigger() { + mOboeAudioOutputStream.trigger(); } public void setChannelEnabled(int channelIndex, boolean enabled) { @@ -57,4 +50,12 @@ public class AudioOutputTester extends AudioStreamTester { public void setSignalType(int type) { mOboeAudioOutputStream.setSignalType(type); } + + public void setAmplitude(float amplitude) { + mOboeAudioOutputStream.setAmplitude(amplitude); + } + + public int getLastErrorCallbackResult() {return mOboeAudioOutputStream.getLastErrorCallbackResult();}; + + public long getFramesRead() {return mOboeAudioOutputStream.getFramesRead();}; } diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioQueryTools.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioQueryTools.java new file mode 100644 index 00000000..c0be795b --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioQueryTools.java @@ -0,0 +1,176 @@ +/* + * Copyright 2017 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. + */ + +package com.mobileer.oboetester; + +import android.content.pm.PackageManager; +import android.media.AudioManager; +import android.os.Build; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.Locale; + +public class AudioQueryTools { + private static String GETPROP_EXECUTABLE_PATH = "/system/bin/getprop"; + + public static String getSystemProperty(String propName) { + Process process = null; + BufferedReader bufferedReader = null; + try { + process = new ProcessBuilder().command(GETPROP_EXECUTABLE_PATH, propName).redirectErrorStream(true).start(); + bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream())); + String line = bufferedReader.readLine(); + if (line == null){ + line = ""; //prop not set + } + return line; + } catch (Exception e) { + return ""; + } finally{ + if (bufferedReader != null){ + try { + bufferedReader.close(); + } catch (IOException e) {} + } + if (process != null){ + process.destroy(); + } + } + } + + public static String getAudioFeatureReport(PackageManager packageManager) { + StringBuffer report = new StringBuffer(); + report.append("\nProAudio Feature : " + + packageManager.hasSystemFeature(PackageManager.FEATURE_AUDIO_PRO)); + report.append("\nLowLatency Feature : " + + packageManager.hasSystemFeature(PackageManager.FEATURE_AUDIO_LOW_LATENCY)); + report.append("\nAudio Output Feature : " + + packageManager.hasSystemFeature(PackageManager.FEATURE_AUDIO_OUTPUT)); + report.append("\nMicrophone Feature : " + + packageManager.hasSystemFeature(PackageManager.FEATURE_MICROPHONE)); + report.append("\nMIDI Feature : " + + packageManager.hasSystemFeature(PackageManager.FEATURE_MIDI)); + report.append("\nUSB Host Feature : " + + packageManager.hasSystemFeature(PackageManager.FEATURE_USB_HOST)); + report.append("\nUSB Accessory Feature: " + + packageManager.hasSystemFeature(PackageManager.FEATURE_USB_ACCESSORY)); + report.append("\nBluetooth Feature : " + + packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH)); + report.append("\nBluetooth LE Feature : " + + packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)); + if (android.os.Build.VERSION.SDK_INT > Build.VERSION_CODES.TIRAMISU) { + report.append("\nTelecom Feature : " + + packageManager.hasSystemFeature(PackageManager.FEATURE_TELECOM)); + report.append("\nTelephonyCall Feature: " + + packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_CALLING)); + } + return report.toString(); + } + + public static String getAudioManagerReport(AudioManager audioManager) { + StringBuffer report = new StringBuffer(); + String unprocessedSupport = audioManager.getProperty( + AudioManager.PROPERTY_SUPPORT_AUDIO_SOURCE_UNPROCESSED); + report.append("\nSUPPORT_AUDIO_SOURCE_UNPROCESSED : " + ((unprocessedSupport == null) ? + "null" : unprocessedSupport)); + String outputFramesPerBuffer = audioManager.getProperty( + AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER); + report.append("\nOUTPUT_FRAMES_PER_BUFFER : " + ((outputFramesPerBuffer == null) ? + "null" : outputFramesPerBuffer)); + String outputSampleRate = audioManager.getProperty( + AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE); + report.append("\nOUTPUT_SAMPLE_RATE : " + ((outputSampleRate == null) ? "null" : + outputSampleRate)); + return report.toString(); + } + + private static String formatKeyValueLine(String key, String value) { + int numSpaces = Math.max(1, 21 - key.length()); + String spaces = String.format(Locale.getDefault(), "%0" + numSpaces + "d", 0).replace("0", " "); + return "\n" + key + spaces + ": " + value; + } + + private static String getSystemPropertyLine(String key) { + return formatKeyValueLine(key, getSystemProperty(key)); + } + + public static String convertSdkToShortName(int sdk) { + if (sdk < 16) return "early"; + if (sdk > 34) return "future"; + final String[] names = { + "J", // 16 + "J+", + "J++", + "K", + "K+", + "L", // 21 + "L+", + "M", + "N", // 24 + "N_MR1", + "O", + "O_MR1", + "P", // 28 + "Q", + "R", + "S", + "S_V2", + "T", // 33 + "U" + }; + return names[sdk - 16]; + } + + public static String getMediaPerformanceClass() { + if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.S) { + return formatKeyValueLine("Media Perf Class", "not supported"); + } + int mpc = Build.VERSION.MEDIA_PERFORMANCE_CLASS; + String text = (mpc == 0) ? "not declared" : convertSdkToShortName(mpc); + return formatKeyValueLine("Media Perf Class", + mpc + " (" + text + ")"); + } + + public static String getAudioPropertyReport() { + StringBuffer report = new StringBuffer(); + report.append(getSystemPropertyLine("aaudio.mmap_policy")); + report.append(getSystemPropertyLine("aaudio.mmap_exclusive_policy")); + report.append(getSystemPropertyLine("aaudio.mixer_bursts")); + report.append(getSystemPropertyLine("aaudio.wakeup_delay_usec")); + report.append(getSystemPropertyLine("aaudio.minimum_sleep_usec")); + report.append(getSystemPropertyLine("aaudio.hw_burst_min_usec")); + report.append(getSystemPropertyLine("aaudio.in_mmap_offset_usec")); + report.append(getSystemPropertyLine("aaudio.out_mmap_offset_usec")); + report.append(getSystemPropertyLine("ro.product.manufacturer")); + report.append(getSystemPropertyLine("ro.product.brand")); + report.append(getSystemPropertyLine("ro.product.model")); + report.append(getSystemPropertyLine("ro.product.name")); + report.append(getSystemPropertyLine("ro.product.device")); + report.append(getSystemPropertyLine("ro.product.cpu.abi")); + report.append(getSystemPropertyLine("ro.soc.manufacturer")); + report.append(getSystemPropertyLine("ro.soc.model")); + report.append(getSystemPropertyLine("ro.arch")); + report.append(getSystemPropertyLine("ro.hardware")); + report.append(getSystemPropertyLine("ro.hardware.chipname")); + report.append(getSystemPropertyLine("ro.board.platform")); + report.append(getSystemPropertyLine("ro.build.changelist")); + report.append(getSystemPropertyLine("ro.build.description")); + report.append(getSystemPropertyLine("ro.build.date")); + return report.toString(); + } +} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AudioRecordThread.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioRecordThread.java similarity index 91% rename from mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AudioRecordThread.java rename to mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioRecordThread.java index 5c7bff0f..bc254552 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AudioRecordThread.java +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioRecordThread.java @@ -14,9 +14,10 @@ * limitations under the License. */ -package com.google.sample.oboe.manualtest; +package com.mobileer.oboetester; +import android.media.AudioDeviceInfo; import android.media.AudioFormat; import android.media.AudioRecord; import android.media.MediaRecorder; @@ -40,6 +41,8 @@ class AudioRecordThread implements Runnable { private int mTaskCountdown; private boolean mCaptureEnabled = true; + private AudioDeviceInfo mDeviceInfo; + public AudioRecordThread(int frameRate, int channelCount, int maxFrames) { mSampleRate = frameRate; mChannelCount = channelCount; @@ -59,6 +62,7 @@ class AudioRecordThread implements Runnable { channelConfig, audioFormat, 2 * minRecordBuffSizeInBytes); + mRecorder.setPreferredDevice(mDeviceInfo); if (mRecorder.getState() == AudioRecord.STATE_UNINITIALIZED) { throw new RuntimeException("Could not make the AudioRecord - UNINITIALIZED"); } @@ -140,6 +144,12 @@ class AudioRecordThread implements Runnable { } } + /** + * Schedule task to be run on its own thread when numSamples more samples have been recorded. + * + * @param numSamples + * @param task + */ public void scheduleTask(int numSamples, Runnable task) { mTask = task; mTaskCountdown = numSamples; @@ -153,4 +163,7 @@ class AudioRecordThread implements Runnable { return mCaptureBuffer.readMostRecent(buffer); } + public void setInputDevice(AudioDeviceInfo deviceInfo) { + mDeviceInfo = deviceInfo; + } } diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AudioStreamBase.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioStreamBase.java similarity index 55% rename from mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AudioStreamBase.java rename to mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioStreamBase.java index 3475bb4a..14eb163e 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AudioStreamBase.java +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioStreamBase.java @@ -14,9 +14,10 @@ * limitations under the License. */ -package com.google.sample.oboe.manualtest; +package com.mobileer.oboetester; import java.io.IOException; +import java.util.Locale; /** * Base class for any audio input or output. @@ -25,9 +26,43 @@ public abstract class AudioStreamBase { private StreamConfiguration mRequestedStreamConfiguration; private StreamConfiguration mActualStreamConfiguration; - + private AudioStreamBase.DoubleStatistics mLatencyStatistics; + private SampleRateMonitor mSampleRateMonitor = new SampleRateMonitor(); private int mBufferSizeInFrames; + private class SampleRateMonitor { + private static final int SIZE = 16; // power of 2 + private static final long MASK = SIZE - 1L; + private long[] times = new long[SIZE]; + private long[] frames = new long[SIZE]; + private long cursor; + + void add(long numFrames) { + int index = (int) (cursor & MASK); + frames[index] = numFrames; + times[index] = System.currentTimeMillis(); + cursor++; + } + + int getRate() { + if (cursor < 2) return 0; + long numValid = Math.min((long)SIZE, cursor); + int oldestIndex = (int)((cursor - numValid) & MASK); + int newestIndex = (int)((cursor - 1) & MASK); + long deltaTime = times[newestIndex] - times[oldestIndex]; + long deltaFrames = frames[newestIndex] - frames[oldestIndex]; + if (deltaTime <= 0) { + return -1; + } + long sampleRate = (deltaFrames * 1000) / deltaTime; + return (int) sampleRate; + } + + void reset() { + cursor = 0; + } + } + public StreamStatus getStreamStatus() { StreamStatus status = new StreamStatus(); status.bufferSize = getBufferSizeInFrames(); @@ -36,11 +71,48 @@ public abstract class AudioStreamBase { status.framesWritten = getFramesWritten(); status.callbackCount = getCallbackCount(); status.latency = getLatency(); + mLatencyStatistics.add(status.latency); + status.callbackTimeStr = getCallbackTimeStr(); status.cpuLoad = getCpuLoad(); status.state = getState(); + mSampleRateMonitor.add(status.framesRead); + status.measuredRate = mSampleRateMonitor.getRate(); return status; } + public DoubleStatistics getLatencyStatistics() { + return mLatencyStatistics; + } + + public void setPerformanceHintEnabled(boolean checked) { + } + public void setHearWorkload(boolean checked) { + } + + public static class DoubleStatistics { + private double sum; + private int count; + private double minimum = Double.MAX_VALUE; + private double maximum = Double.MIN_VALUE; + + void add(double statistic) { + if (statistic <= 0.0) return; + sum += statistic; + count++; + minimum = Math.min(statistic, minimum); + maximum = Math.max(statistic, maximum); + } + + double getAverage() { + return sum / count; + } + + public String dump() { + if (count == 0) return "?"; + return String.format(Locale.getDefault(), "%3.1f/%3.1f/%3.1f ms", minimum, getAverage(), maximum); + } + } + /** * Changes dynamic at run-time. */ @@ -53,7 +125,9 @@ public abstract class AudioStreamBase { public int state; public long callbackCount; public int framesPerCallback; - public double cpuLoad; + public float cpuLoad; + public String callbackTimeStr; + public int measuredRate; // These are constantly changing. String dump(int framesPerBurst) { @@ -62,30 +136,31 @@ public abstract class AudioStreamBase { } StringBuffer buffer = new StringBuffer(); - buffer.append("frames written " + framesWritten + " - read " + framesRead - + " = " + (framesWritten - framesRead) + "\n"); + buffer.append("time between callbacks = " + callbackTimeStr + "\n"); - String latencyText = (latency < 0.0) - ? "?" - : String.format("%6.1f ms", latency); - String cpuLoadText = String.format("%2d%c", (int)(cpuLoad * 100), '%'); + buffer.append("wr " + + String.format(Locale.getDefault(), "%Xh", framesWritten) + + " - rd " + String.format(Locale.getDefault(), "%Xh", framesRead) + + " = " + (framesWritten - framesRead) + " fr" + + ", SR = " + ((measuredRate <= 0) ? "?" : measuredRate) + "\n"); + + String cpuLoadText = String.format(Locale.getDefault(), "%2d%c", (int)(cpuLoad * 100), '%'); buffer.append( convertStateToString(state) + ", #cb=" + callbackCount - + ", f/cb=" + String.format("%3d", framesPerCallback) - + ", latnc = " + latencyText - + ", " + cpuLoadText + " cpu" + + ", f/cb=" + String.format(Locale.getDefault(), "%3d", framesPerCallback) + + ", " + cpuLoadText + " CPU" + "\n"); buffer.append("buffer size = "); - if (bufferSize < 0) { + if (bufferSize <= 0 || framesPerBurst <= 0) { buffer.append("?"); } else { int numBuffers = bufferSize / framesPerBurst; int remainder = bufferSize - (numBuffers * framesPerBurst); buffer.append(bufferSize + " = (" + numBuffers + " * " + framesPerBurst + ") + " + remainder); } - buffer.append(", xRun# = " + ((xRunCount < 0) ? "?" : xRunCount) + "\n"); + buffer.append(", xRun# = " + ((xRunCount < 0) ? "?" : xRunCount)); return buffer.toString(); } @@ -116,6 +191,14 @@ public abstract class AudioStreamBase { mRequestedStreamConfiguration = requestedConfiguration; mActualStreamConfiguration = actualConfiguration; mBufferSizeInFrames = bufferSizeInFrames; + mLatencyStatistics = new AudioStreamBase.DoubleStatistics(); + } + + public void onStart() { + mSampleRateMonitor.reset(); + } + public void onStop() { + mSampleRateMonitor.reset(); } public abstract boolean isInput(); @@ -124,8 +207,6 @@ public abstract class AudioStreamBase { public void stopPlayback() throws IOException {} - public abstract int write(float[] buffer, int offset, int length); - public abstract void close(); public int getChannelCount() { @@ -154,23 +235,24 @@ public abstract class AudioStreamBase { public long getCallbackCount() { return -1; } + public int getLastErrorCallbackResult() { return 0; } + public long getFramesWritten() { return -1; } public long getFramesRead() { return -1; } public double getLatency() { return -1.0; } - public double getCpuLoad() { return 0.0; } + public float getCpuLoad() { return 0.0f; } + public float getAndResetMaxCpuLoad() { return 0.0f; } + public int getAndResetCpuMask() { return 0; } + + public String getCallbackTimeStr() { return "?"; }; public int getState() { return -1; } - public boolean isThresholdSupported() { - return false; - } - - public void setWorkload(double workload) {} + public void setWorkload(int workload) {} public abstract int getXRunCount(); - } diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AudioStreamTester.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioStreamTester.java similarity index 93% rename from mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AudioStreamTester.java rename to mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioStreamTester.java index 7d0a50ed..be740b3c 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AudioStreamTester.java +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioStreamTester.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.sample.oboe.manualtest; +package com.mobileer.oboetester; import java.io.IOException; @@ -33,6 +33,7 @@ class AudioStreamTester { } public void reset() { + setWorkload(0); requestedConfiguration.reset(); // TODO consider making new ones actualConfiguration.reset(); } @@ -45,7 +46,7 @@ class AudioStreamTester { mCurrentAudioStream.startPlayback(); } - public void setWorkload(double workload) { + public void setWorkload(int workload) { mCurrentAudioStream.setWorkload(workload); } } diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AutomatedGlitchActivity.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AutomatedGlitchActivity.java new file mode 100644 index 00000000..ae53a7f6 --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AutomatedGlitchActivity.java @@ -0,0 +1,153 @@ +/* + * 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. + */ + +package com.mobileer.oboetester; + +import android.os.Bundle; +import android.view.View; +import android.widget.AdapterView; +import android.widget.Spinner; + +/** + * Look for glitches with various configurations. + * A sine wave is played and continuously recorded using loopback. + * An analyzer locks to the phase and magnitude of the detected sine wave. + * It then compares the incoming signal with a predicted sine wave. + */ +public class AutomatedGlitchActivity extends BaseAutoGlitchActivity { + + private Spinner mDurationSpinner; + + // Test with these configurations. + private static final int[] PERFORMANCE_MODES = { + StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY, + StreamConfiguration.PERFORMANCE_MODE_NONE + }; + private static final int[] SAMPLE_RATES = { 48000, 44100, 16000 }; + private static final int MONO = 1; + private static final int STEREO = 2; + private static final int UNSPECIFIED = 0; + + private class DurationSpinnerListener implements android.widget.AdapterView.OnItemSelectedListener { + @Override + public void onItemSelected(AdapterView parent, View view, int pos, long id) { + String text = parent.getItemAtPosition(pos).toString(); + mDurationSeconds = Integer.parseInt(text); + } + + @Override + public void onNothingSelected(AdapterView parent) { + mDurationSeconds = DEFAULT_DURATION_SECONDS; + } + } + + @Override + protected void inflateActivity() { + setContentView(R.layout.activity_auto_glitches); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mDurationSpinner = (Spinner) findViewById(R.id.spinner_glitch_duration); + mDurationSpinner.setOnItemSelectedListener(new DurationSpinnerListener()); + + setAnalyzerText(getString(R.string.auto_glitch_instructions)); + } + + @Override + public String getTestName() { + return "AutoGlitch"; + } + + private void testConfiguration(int perfMode, + int sharingMode, + int sampleRate, + int inChannels, + int outChannels) throws InterruptedException { + + // Configure settings + StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration; + StreamConfiguration requestedOutConfig = mAudioOutTester.requestedConfiguration; + + requestedInConfig.reset(); + requestedOutConfig.reset(); + + requestedInConfig.setPerformanceMode(perfMode); + requestedOutConfig.setPerformanceMode(perfMode); + + requestedInConfig.setSharingMode(sharingMode); + requestedOutConfig.setSharingMode(sharingMode); + + requestedInConfig.setSampleRate(sampleRate); + requestedOutConfig.setSampleRate(sampleRate); + + requestedInConfig.setChannelCount(inChannels); + requestedOutConfig.setChannelCount(outChannels); + + testCurrentConfigurations(); + } + + private void testConfiguration(int performanceMode, + int sharingMode, + int sampleRate) throws InterruptedException { + testConfiguration(performanceMode, + sharingMode, + sampleRate, MONO, STEREO); + testConfiguration(performanceMode, + sharingMode, + sampleRate, STEREO, MONO); + } + + @Override + public void runTest() { + try { + logDeviceInfo(); + + mTestResults.clear(); + + // Test with STEREO on both input and output. + testConfiguration(StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY, + StreamConfiguration.SHARING_MODE_EXCLUSIVE, + UNSPECIFIED, STEREO, STEREO); + + // Test EXCLUSIVE mode with a configuration most likely to work. + testConfiguration(StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY, + StreamConfiguration.SHARING_MODE_EXCLUSIVE, + UNSPECIFIED); + + // Test various combinations. + for (int perfMode : PERFORMANCE_MODES) { + for (int sampleRate : SAMPLE_RATES) { + testConfiguration(perfMode, + StreamConfiguration.SHARING_MODE_SHARED, + sampleRate); + } + } + + compareFailedTestsWithNearestPassingTest(); + + } catch (InterruptedException e) { + compareFailedTestsWithNearestPassingTest(); + + } catch (Exception e) { + log(e.getMessage()); + showErrorToast(e.getMessage()); + } + } + +} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AutomatedTestRunner.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AutomatedTestRunner.java new file mode 100644 index 00000000..51eb22bc --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AutomatedTestRunner.java @@ -0,0 +1,338 @@ +package com.mobileer.oboetester; + +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.util.AttributeSet; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.WindowManager; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.ScrollView; +import android.widget.TextView; + +import androidx.annotation.NonNull; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; + +/** + * Run an automated test from a UI, gather logs, + * and display a summary. + */ +public class AutomatedTestRunner extends LinearLayout implements Runnable { + + private Button mStartButton; + private Button mStopButton; + private Button mShareButton; + private TextView mAutoTextView; + private ScrollView mAutoTextScroller; + private TextView mSingleTestIndex; + private StringBuffer mFailedSummary; + private StringBuffer mSummary; + private StringBuffer mFullSummary; + private int mTestCount; + private int mPassCount; + private int mFailCount; + private TestAudioActivity mActivity; + + private Thread mAutoThread; + private volatile boolean mThreadEnabled; + private CachedTextViewLog mCachedTextView; + + public AutomatedTestRunner(Context context) { + super(context); + initializeViews(context); + } + + public AutomatedTestRunner(Context context, AttributeSet attrs) { + super(context, attrs); + initializeViews(context); + } + + public AutomatedTestRunner(Context context, + AttributeSet attrs, + int defStyle) { + super(context, attrs, defStyle); + initializeViews(context); + } + + public TestAudioActivity getActivity() { + return mActivity; + } + + public void setActivity(TestAudioActivity activity) { + this.mActivity = activity; + mCachedTextView = new CachedTextViewLog(activity, mAutoTextView); + } + + /** + * Inflates the views in the layout. + * + * @param context + * the current context for the view. + */ + private void initializeViews(Context context) { + LayoutInflater inflater = (LayoutInflater) context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.auto_test_runner, this); + + mStartButton = (Button) findViewById(R.id.button_start); + mStartButton.setOnClickListener( new OnClickListener() { + @Override + public void onClick(View v) { + startTest(); + } + }); + + mStopButton = (Button) findViewById(R.id.button_stop); + mStopButton.setOnClickListener( new OnClickListener() { + @Override + public void onClick(View v) { + stopTest(); + } + }); + + mShareButton = (Button) findViewById(R.id.button_share); + mShareButton.setOnClickListener( new OnClickListener() { + @Override + public void onClick(View v) { + shareResult(); + mShareButton.setEnabled(true); + } + }); + mShareButton.setEnabled(false); + + mSingleTestIndex = (TextView) findViewById(R.id.single_test_index); + + mAutoTextScroller = (ScrollView) findViewById(R.id.text_log_auto_scroller); + mAutoTextView = (TextView) findViewById(R.id.text_log_auto); + + mFailedSummary = new StringBuffer(); + mSummary = new StringBuffer(); + mFullSummary = new StringBuffer(); + } + + private void updateStartStopButtons(boolean running) { + mStartButton.setEnabled(!running); + mStopButton.setEnabled(running); + } + + public int getTestCount() { + return mTestCount; + } + + public boolean isThreadEnabled() { + return mThreadEnabled; + } + + public void appendFailedSummary(String text) { + mFailedSummary.append(text); + } + + public void appendSummary(String text) { + mSummary.append(text); + } + + public void incrementFailCount() { + mFailCount++; + } + public void incrementPassCount() { + mPassCount++; + } + public void incrementTestCount() { + mTestCount++; + } + + // Write to scrollable TextView + public void log(final String text) { + if (text == null) return; + Log.d(TestAudioActivity.TAG, "LOG - " + text); + mCachedTextView.append(text + "\n"); + mFullSummary.append(text + "\n"); + scrollToBottom(); + } + + public void scrollToBottom() { + mAutoTextScroller.fullScroll(View.FOCUS_DOWN); + } + + // Flush any logs that are stuck in the cache. + public void flushLog() { + mCachedTextView.flush(); + scrollToBottom(); + } + + private void logClear() { + mCachedTextView.clear(); + mFullSummary.delete(0, mFullSummary.length()); + mSummary.delete(0, mSummary.length()); + mFailedSummary.delete(0, mFailedSummary.length()); + } + + protected String getFullLogs() { + return mFullSummary.toString(); + } + + private void startAutoThread() { + mThreadEnabled = true; + mAutoThread = new Thread(this); + mAutoThread.start(); + } + + private void stopAutoThread() { + try { + if (mAutoThread != null) { + Log.d(TestAudioActivity.TAG, + "Who called stopAutoThread()?", + new RuntimeException("Just for debugging.")); + mThreadEnabled = false; + mAutoThread.interrupt(); + mAutoThread.join(100); + mAutoThread = null; + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + protected void setTestIndexText(int newTestIndex) { + if (newTestIndex >= 0) { + mSingleTestIndex.setText(String.valueOf(newTestIndex)); + } else { + mSingleTestIndex.setText(""); + } + } + + private void updateTestIndex() { + CharSequence chars = mSingleTestIndex.getText(); + String text = chars.toString(); + int testIndex = -1; + String trimmed = chars.toString().trim(); + if (trimmed.length() > 0) { + try { + testIndex = Integer.parseInt(text); + } catch (NumberFormatException e) { + mActivity.showErrorToast("Badly formated callback size: " + text); + mSingleTestIndex.setText(""); + } + } + mActivity.setSingleTestIndex(testIndex); + } + + protected void startTest() { + updateTestIndex(); + updateStartStopButtons(true); + startAutoThread(); + } + + public void stopTest() { + stopAutoThread(); + } + + // Only call from UI thread. + public void onTestStarted() { + mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + + // Only call from UI thread. + public void onTestFinished() { + mActivity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + updateStartStopButtons(false); + mShareButton.setEnabled(true); + } + + public static String getTimestampString() { + DateFormat df = new SimpleDateFormat("yyyyMMdd-HHmmss"); + Date now = Calendar.getInstance().getTime(); + return df.format(now); + } + + // Share text from log via GMail, Drive or other method. + public void shareResult() { + Intent sharingIntent = new Intent(android.content.Intent.ACTION_SEND); + sharingIntent.setType("text/plain"); + + String subjectText = "OboeTester-" + mActivity.getTestName() + + "-" + Build.MANUFACTURER + + "-" + Build.MODEL + + "-" + getTimestampString(); + subjectText = subjectText.replace(' ', '-'); + sharingIntent.putExtra(android.content.Intent.EXTRA_SUBJECT, subjectText); + + String shareBody = mAutoTextView.getText().toString(); + sharingIntent.putExtra(android.content.Intent.EXTRA_TEXT, shareBody); + + mActivity.startActivity(Intent.createChooser(sharingIntent, "Share using:")); + } + + @Override + public void run() { + mActivity.runOnUiThread(new Runnable() { + @Override + public void run() { + onTestStarted(); + } + }); + logClear(); + log("=== STARTED at " + new Date()); + log(mActivity.getTestName()); + log(MainActivity.getVersionText()); + log(Build.MANUFACTURER + ", " + Build.MODEL + ", " + Build.PRODUCT); + log(Build.DISPLAY); + appendFailedSummary("Summary\n"); + mTestCount = 0; + mPassCount = 0; + mFailCount = 0; + try { + mActivity.runTest(); + log("Tests finished."); + } catch(Exception e) { + log("EXCEPTION: " + e.getMessage()); + } finally { + mActivity.stopTest(); + if (!mThreadEnabled) { + log("== TEST STOPPED =="); + } + log("\n==== SUMMARY ========"); + log(mSummary.toString()); + if (mFailCount > 0) { + log("These tests FAILED:"); + log(mFailedSummary.toString()); + log("------------"); + } else if (mPassCount > 0) { + log("All " + mPassCount + " tests PASSED."); + } else { + log("No tests were run!"); + } + log(getPassFailReport()); + log("== FINISHED at " + new Date()); + + flushLog(); + + mActivity.saveIntentLog(); + + mActivity.runOnUiThread(new Runnable() { + @Override + public void run() { + onTestFinished(); + flushLog(); + } + }); + } + } + + @NonNull + public String getPassFailReport() { + int skipped = mTestCount - (mPassCount + mFailCount); + String passFailReport = mPassCount + " passed. " + + mFailCount + " failed. " + + skipped + " skipped. "; + return passFailReport; + } + +} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/BaseAutoGlitchActivity.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/BaseAutoGlitchActivity.java new file mode 100644 index 00000000..3256fad5 --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/BaseAutoGlitchActivity.java @@ -0,0 +1,613 @@ +/* + * 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. + */ + +package com.mobileer.oboetester; + +import static com.mobileer.oboetester.StreamConfiguration.convertChannelMaskToText; + +import android.content.Context; +import android.media.AudioDeviceInfo; +import android.media.AudioManager; +import android.os.Bundle; +import android.os.Environment; + +import androidx.annotation.Nullable; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Locale; + +public class BaseAutoGlitchActivity extends GlitchActivity { + + private static final int SETUP_TIME_SECONDS = 4; // Time for the stream to settle. + protected static final int DEFAULT_DURATION_SECONDS = 8; // Run time for each test. + private static final int DEFAULT_GAP_MILLIS = 400; // Idle time between each test. + private static final String TEXT_SKIP = "SKIP"; + public static final String TEXT_PASS = "PASS"; + public static final String TEXT_FAIL = "FAIL !!!!"; + + protected int mDurationSeconds = DEFAULT_DURATION_SECONDS; + protected int mGapMillis = DEFAULT_GAP_MILLIS; + private String mTestName = ""; + + protected AudioManager mAudioManager; + + protected ArrayList mTestResults = new ArrayList(); + + public static boolean arrayContains(int[] haystack, int needle) { + for (int n: haystack) { + if (n == needle) return true; + } + return false; + } + + void logDeviceInfo() { + log("\n############################"); + log("\nDevice Info:"); + AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + log(AudioQueryTools.getAudioManagerReport(audioManager)); + log(AudioQueryTools.getAudioFeatureReport(getPackageManager())); + log(AudioQueryTools.getAudioPropertyReport()); + log("\n############################"); + } + + void setTestName(String name) { + mTestName = name; + } + + @Override + public int getDeviceId() { + return super.getDeviceId(); + } + + private static class TestStreamOptions { + public final int channelUsed; + public final int channelCount; + public final int channelMask; + public final int deviceId; + public final int mmapUsed; + public final int performanceMode; + public final int sharingMode; + public final int sessionId; + + public TestStreamOptions(StreamConfiguration configuration, int channelUsed) { + this.channelUsed = channelUsed; + channelCount = configuration.getChannelCount(); + channelMask = configuration.getChannelMask(); + deviceId = configuration.getDeviceId(); + mmapUsed = configuration.isMMap() ? 1 : 0; + performanceMode = configuration.getPerformanceMode(); + sharingMode = configuration.getSharingMode(); + sessionId = configuration.getSessionId(); + } + + int countDifferences(TestStreamOptions other) { + int count = 0; + count += (channelUsed != other.channelUsed) ? 1 : 0; + count += (channelCount != other.channelCount) ? 1 : 0; + count += (channelMask != other.channelMask) ? 1 : 0; + count += (deviceId != other.deviceId) ? 1 : 0; + count += (mmapUsed != other.mmapUsed) ? 1 : 0; + count += (performanceMode != other.performanceMode) ? 1 : 0; + count += (sharingMode != other.sharingMode) ? 1 : 0; + count += (sessionId != other.sessionId) ? 1 : 0; + return count; + } + + public String comparePassedDirection(String prefix, TestStreamOptions passed) { + StringBuffer text = new StringBuffer(); + text.append(TestDataPathsActivity.comparePassedField(prefix, this, passed, "channelUsed")); + text.append(TestDataPathsActivity.comparePassedField(prefix,this, passed, "channelCount")); + text.append(TestDataPathsActivity.comparePassedField(prefix,this, passed, "channelMask")); + text.append(TestDataPathsActivity.comparePassedField(prefix,this, passed, "deviceId")); + text.append(TestDataPathsActivity.comparePassedField(prefix,this, passed, "mmapUsed")); + text.append(TestDataPathsActivity.comparePassedField(prefix,this, passed, "performanceMode")); + text.append(TestDataPathsActivity.comparePassedField(prefix,this, passed, "sharingMode")); + text.append(TestDataPathsActivity.comparePassedField(prefix,this, passed, "sessionId")); + return text.toString(); + } + @Override + public String toString() { + return "D=" + deviceId + + ", " + ((mmapUsed > 0) ? "MMAP" : "Lgcy") + + ", ch=" + channelText(channelUsed, channelCount) + + ", cm=" + convertChannelMaskToText(channelMask) + + "," + StreamConfiguration.convertPerformanceModeToText(performanceMode) + + "," + StreamConfiguration.convertSharingModeToText(sharingMode); + } + } + + protected static class TestResult { + final int testIndex; + final TestStreamOptions input; + final TestStreamOptions output; + public final int inputPreset; + public final int sampleRate; + final String testName; // name or purpose of test + + int result = TEST_RESULT_SKIPPED; // TEST_RESULT_FAILED, etc + private String mComments = ""; // additional info, ideas for why it failed + + public TestResult(int testIndex, + String testName, + StreamConfiguration inputConfiguration, + int inputChannel, + StreamConfiguration outputConfiguration, + int outputChannel) { + this.testIndex = testIndex; + this.testName = testName; + input = new TestStreamOptions(inputConfiguration, inputChannel); + output = new TestStreamOptions(outputConfiguration, outputChannel); + sampleRate = outputConfiguration.getSampleRate(); + this.inputPreset = inputConfiguration.getInputPreset(); + } + + int countDifferences(TestResult other) { + int count = 0; + count += input.countDifferences((other.input)); + count += output.countDifferences((other.output)); + count += (sampleRate != other.sampleRate) ? 1 : 0; + count += (inputPreset != other.inputPreset) ? 1 : 0; + return count; + } + + public boolean failed() { + return result == TEST_RESULT_FAILED; + } + + public boolean passed() { + return result == TEST_RESULT_PASSED; + } + + public String comparePassed(TestResult passed) { + StringBuffer text = new StringBuffer(); + text.append("Compare with passed test #" + passed.testIndex + "\n"); + text.append(input.comparePassedDirection("IN", passed.input)); + text.append(TestDataPathsActivity.comparePassedInputPreset("IN", this, passed)); + text.append(output.comparePassedDirection("OUT", passed.output)); + text.append(TestDataPathsActivity.comparePassedField("I/O",this, passed, "sampleRate")); + + return text.toString(); + } + + @Override + public String toString() { + return "IN: " + input + ", ip=" + inputPreset + "\n" + + "OUT: " + output + ", sr=" + sampleRate + + mComments; + } + + public void addComment(String comment) { + mComments += "\n"; + mComments += comment; + } + + public void setResult(int result) { + this.result = result; + } + public int getResult(int result) { + return result; + } + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + + mAutomatedTestRunner = findViewById(R.id.auto_test_runner); + mAutomatedTestRunner.setActivity(this); + } + + protected void log(String text) { + mAutomatedTestRunner.log(text); + } + + protected void appendFailedSummary(String text) { + mAutomatedTestRunner.appendFailedSummary(text); + } + protected void appendSummary(String text) { + mAutomatedTestRunner.appendSummary(text); + } + + @Override + public void onStopTest() { + mAutomatedTestRunner.stopTest(); + } + + static String channelText(int index, int count) { + return index + "/" + count; + } + + protected String getConfigText(StreamConfiguration config) { + int channel = (config.getDirection() == StreamConfiguration.DIRECTION_OUTPUT) + ? getOutputChannel() : getInputChannel(); + return ((config.getDirection() == StreamConfiguration.DIRECTION_OUTPUT) ? "OUT" : "INP") + + (config.isMMap() ? "-M" : "-L") + + "-" + StreamConfiguration.convertSharingModeToText(config.getSharingMode()) + + ", ID = " + String.format(Locale.getDefault(), "%2d", config.getDeviceId()) + + ", Perf = " + StreamConfiguration.convertPerformanceModeToText( + config.getPerformanceMode()) + + ((config.getSessionId() > 0) ? (", sessionId = " + config.getSessionId()) : "") + + ",\n ch = " + channelText(channel, config.getChannelCount()) + + ", cm = " + convertChannelMaskToText(config.getChannelMask()); + } + + protected String getStreamText(AudioStreamBase stream) { + return ("burst=" + stream.getFramesPerBurst() + + ", size=" + stream.getBufferSizeInFrames() + + ", cap=" + stream.getBufferCapacityInFrames() + ); + } + + public final static int TEST_RESULT_FAILED = -2; + public final static int TEST_RESULT_WARNING = -1; + public final static int TEST_RESULT_SKIPPED = 0; + public final static int TEST_RESULT_PASSED = 1; + + // Run one test based on the requested input/output configurations. + @Nullable + protected TestResult testCurrentConfigurations() throws InterruptedException { + mAutomatedTestRunner.incrementTestCount(); + if ((getSingleTestIndex() >= 0) && (getTestCount() != getSingleTestIndex())) { + return null; + } + + log("========================== #" + getTestCount()); + int result = 0; + StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration; + StreamConfiguration requestedOutConfig = mAudioOutTester.requestedConfiguration; + + StreamConfiguration actualInConfig = mAudioInputTester.actualConfiguration; + StreamConfiguration actualOutConfig = mAudioOutTester.actualConfiguration; + + log("Requested:"); + log(" SR = " + requestedOutConfig.getSampleRate()); + log(" " + getConfigText(requestedInConfig)); + log(" " + getConfigText(requestedOutConfig)); + + String reason = ""; + boolean openFailed = false; + try { + openAudio(); // this will fill in actualConfig + + log("Actual:"); + log(" SR = " + actualOutConfig.getSampleRate()); + // Set output size to a level that will avoid glitches. + AudioStreamBase outStream = mAudioOutTester.getCurrentAudioStream(); + int sizeFrames = outStream.getBufferCapacityInFrames() / 2; + sizeFrames = Math.max(sizeFrames, 2 * outStream.getFramesPerBurst()); + outStream.setBufferSizeInFrames(sizeFrames); + AudioStreamBase inStream = mAudioInputTester.getCurrentAudioStream(); + log(" " + getConfigText(actualInConfig)); + log(" " + getStreamText(inStream)); + log(" " + getConfigText(actualOutConfig)); + log(" " + getStreamText(outStream)); + } catch (Exception e) { + openFailed = true; + log(e.getMessage()); + reason = e.getMessage(); + } + + TestResult testResult = new TestResult( + getTestCount(), + mTestName, + mAudioInputTester.actualConfiguration, + getInputChannel(), + mAudioOutTester.actualConfiguration, + getOutputChannel() + ); + + // The test will only be worth running if we got the configuration we requested on input or output. + String skipReason = whyShouldTestBeSkipped(); + boolean skipped = skipReason.length() > 0; + boolean valid = !openFailed && !skipped; + boolean startFailed = false; + if (valid) { + try { + startAudioTest(); // Start running the test in the background. + } catch (IOException e) { + e.printStackTrace(); + valid = false; + startFailed = true; + log(e.getMessage()); + reason = e.getMessage(); + } + } + mAutomatedTestRunner.flushLog(); + + if (valid) { + // Check for early return until we reach full duration. + long now = System.currentTimeMillis(); + long startedAt = now; + long endTime = System.currentTimeMillis() + (mDurationSeconds * 1000); + boolean finishedEarly = false; + while (now < endTime && !finishedEarly) { + Thread.sleep(100); // Let test run. + now = System.currentTimeMillis(); + finishedEarly = isFinishedEarly(); + if (finishedEarly) { + log("Finished early after " + (now - startedAt) + " msec."); + } + } + } + int inXRuns = 0; + int outXRuns = 0; + + if (!openFailed) { + // get xRuns before closing the streams. + inXRuns = mAudioInputTester.getCurrentAudioStream().getXRunCount(); + outXRuns = mAudioOutTester.getCurrentAudioStream().getXRunCount(); + + super.stopAudioTest(); + } + + if (openFailed || startFailed) { + appendFailedSummary("------ #" + getTestCount() + "\n"); + appendFailedSummary(getConfigText(requestedInConfig) + "\n"); + appendFailedSummary(getConfigText(requestedOutConfig) + "\n"); + appendFailedSummary(reason + "\n"); + mAutomatedTestRunner.incrementFailCount(); + } else if (skipped) { + log(TEXT_SKIP + " - " + skipReason); + } else { + log("Result:"); + reason += didTestFail(); + boolean passed = reason.length() == 0; + + String resultText = getShortReport(); + resultText += ", xruns = " + inXRuns + "/" + outXRuns; + resultText += ", " + (passed ? TEXT_PASS : TEXT_FAIL); + resultText += reason; + log(" " + resultText); + if (!passed) { + appendFailedSummary("------ #" + getTestCount() + "\n"); + appendFailedSummary(" " + getConfigText(actualInConfig) + "\n"); + appendFailedSummary(" " + getConfigText(actualOutConfig) + "\n"); + appendFailedSummary(" " + resultText + "\n"); + saveRecordingAsWave(); + mAutomatedTestRunner.incrementFailCount(); + result = TEST_RESULT_FAILED; + } else { + mAutomatedTestRunner.incrementPassCount(); + result = TEST_RESULT_PASSED; + } + } + mAutomatedTestRunner.flushLog(); + + // Give hardware time to settle between tests. + Thread.sleep(mGapMillis); + + if (valid) { + testResult.setResult(result); + mTestResults.add(testResult); + } + return testResult; + } + + private void saveRecordingAsWave() { + File recordingDir = getExternalFilesDir(Environment.DIRECTORY_MUSIC); + File waveFile = new File(recordingDir, String.format("glitch_%03d.wav", getTestCount())); + int saveResult = saveWaveFile(waveFile.getAbsolutePath()); + if (saveResult > 0) { + appendFailedSummary("Saved in " + waveFile.getAbsolutePath() + "\n"); + } else { + appendFailedSummary("saveWaveFile() returned " + saveResult + "\n"); + } + } + + protected int getTestCount() { + return mAutomatedTestRunner.getTestCount(); + } + + protected AudioDeviceInfo getDeviceInfoById(int deviceId) { + AudioDeviceInfo[] devices = mAudioManager.getDevices(AudioManager.GET_DEVICES_ALL); + for (AudioDeviceInfo deviceInfo : devices) { + if (deviceInfo.getId() == deviceId) { + return deviceInfo; + } + } + return null; + } + + protected AudioDeviceInfo getDeviceInfoByType(int deviceType, int flags) { + AudioDeviceInfo[] devices = mAudioManager.getDevices(flags); + for (AudioDeviceInfo deviceInfo : devices) { + if (deviceInfo.getType() == deviceType) { + return deviceInfo; + } + } + return null; + } + + /** + * Are outputs mixed in the air or by a loopback plug? + * @param type device type, eg AudioDeviceInfo.TYPE_BUILTIN_SPEAKER + * @return true if stereo output channels get mixed to mono input + */ + protected boolean isDeviceTypeMixedForLoopback(int type) { + switch(type) { + // Mixed in the air. + case AudioDeviceInfo.TYPE_BUILTIN_SPEAKER: + case AudioDeviceInfo.TYPE_BUILTIN_SPEAKER_SAFE: + // Mixed in the loopback fun-plug. + case AudioDeviceInfo.TYPE_WIRED_HEADSET: + case AudioDeviceInfo.TYPE_USB_HEADSET: + return true; + + case AudioDeviceInfo.TYPE_USB_DEVICE: + default: + return false; // channels are discrete + } + } + + /** + * @param type + * @return list of compatible device types in preferred order + */ + protected ArrayList getCompatibleDeviceTypes(int type) { + ArrayList compatibleTypes = new ArrayList(); + switch(type) { + case AudioDeviceInfo.TYPE_BUILTIN_SPEAKER: + case AudioDeviceInfo.TYPE_BUILTIN_SPEAKER_SAFE: + compatibleTypes.add(AudioDeviceInfo.TYPE_BUILTIN_MIC); + break; + case AudioDeviceInfo.TYPE_BUILTIN_MIC: + compatibleTypes.add(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER); + break; + case AudioDeviceInfo.TYPE_USB_DEVICE: + // Give priority to an exact match of DEVICE. + compatibleTypes.add(AudioDeviceInfo.TYPE_USB_DEVICE); + compatibleTypes.add(AudioDeviceInfo.TYPE_USB_HEADSET); + break; + case AudioDeviceInfo.TYPE_USB_HEADSET: + // Give priority to an exact match of HEADSET. + compatibleTypes.add(AudioDeviceInfo.TYPE_USB_HEADSET); + compatibleTypes.add(AudioDeviceInfo.TYPE_USB_DEVICE); + break; + default: + compatibleTypes.add(type); + break; + } + return compatibleTypes; + } + + /** + * Scan available device for one with a compatible device type for loopback testing. + * @return deviceId + */ + + protected AudioDeviceInfo findCompatibleInputDevice(int outputDeviceType) { + ArrayList compatibleDeviceTypes = getCompatibleDeviceTypes(outputDeviceType); + AudioDeviceInfo[] devices = mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS); + // Scan the compatible types in order of preference. + for (int compatibleType : compatibleDeviceTypes) { + for (AudioDeviceInfo candidate : devices) { + if (candidate.getType() == compatibleType) { + return candidate; + } + } + } + return null; + } + + protected boolean isFinishedEarly() { + return false; + } + + /** + * Figure out if a test should be skipped and return the reason. + * + * @return reason for skipping or an empty string + */ + protected String whyShouldTestBeSkipped() { + String why = ""; + StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration; + StreamConfiguration requestedOutConfig = mAudioOutTester.requestedConfiguration; + StreamConfiguration actualInConfig = mAudioInputTester.actualConfiguration; + StreamConfiguration actualOutConfig = mAudioOutTester.actualConfiguration; + // No point running the test if we don't get any of the sharing modes we requested. + if (actualInConfig.getSharingMode() != requestedInConfig.getSharingMode() + && actualOutConfig.getSharingMode() != requestedOutConfig.getSharingMode()) { + log("Did not get requested sharing mode."); + why += "share,"; + } + if (actualInConfig.getPerformanceMode() != requestedInConfig.getPerformanceMode() + && actualOutConfig.getPerformanceMode() != requestedOutConfig.getPerformanceMode()) { + log("Did not get requested performance mode."); + why += "perf,"; + } + if (actualInConfig.isMMap() != requestedInConfig.isMMap() + && actualOutConfig.isMMap() != requestedOutConfig.isMMap()) { + log("Did not get requested MMAP data path."); + why += "mmap,"; + } + return why; + } + + public String didTestFail() { + String why = ""; + if (getMaxSecondsWithNoGlitch() <= (mDurationSeconds - SETUP_TIME_SECONDS)) { + why += ", glitch"; + } + return why; + } + + void logAnalysis(String text) { + appendFailedSummary(text + "\n"); + } + + private int countPassingTests() { + int numPassed = 0; + for (TestResult other : mTestResults) { + if (other.passed()) { + numPassed++; + } + } + return numPassed; + } + + protected void compareFailedTestsWithNearestPassingTest() { + logAnalysis("\n==== COMPARISON ANALYSIS ==========="); + if (countPassingTests() == 0) { + logAnalysis("Comparison skipped because NO tests passed."); + return; + } + logAnalysis("Compare failed tests with others that passed."); + // Analyze each failed test. + for (TestResult testResult : mTestResults) { + if (testResult.failed()) { + logAnalysis("-------------------- #" + testResult.testIndex + " FAILED"); + String name = testResult.testName; + if (name.length() > 0) { + logAnalysis(name); + } + TestResult[] closest = findClosestPassingTestResults(testResult); + for (TestResult other : closest) { + logAnalysis(testResult.comparePassed(other)); + } + logAnalysis(testResult.toString()); + } + } + } + + + @Nullable + private TestResult[] findClosestPassingTestResults(TestResult testResult) { + int minDifferences = Integer.MAX_VALUE; + for (TestResult other : mTestResults) { + if (other.passed()) { + int numDifferences = testResult.countDifferences(other); + if (numDifferences < minDifferences) { + minDifferences = numDifferences; + } + } + } + // Now find all the tests that are just as close as the closest. + ArrayList list = new ArrayList(); + for (TestResult other : mTestResults) { + if (other.passed()) { + int numDifferences = testResult.countDifferences(other); + if (numDifferences == minDifferences) { + list.add(other); + } + } + } + return list.toArray(new TestResult[0]); + } + +} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/BaseOboeTesterActivity.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/BaseOboeTesterActivity.java new file mode 100644 index 00000000..a82ff4ff --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/BaseOboeTesterActivity.java @@ -0,0 +1,108 @@ +/* + * 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. + */ + +package com.mobileer.oboetester; + +import android.Manifest; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.ActivityCompat; + +/** + * Support requesting RECORD_AUDIO permission. + */ + +public abstract class BaseOboeTesterActivity extends AppCompatActivity + implements ActivityCompat.OnRequestPermissionsResultCallback { + + private static final int MY_PERMISSIONS_REQUEST_RECORD_AUDIO = 938355; + private Class mTestClass; + + protected void showErrorToast(String message) { + showToast("Error: " + message); + } + + protected void showToast(final String message) { + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(BaseOboeTesterActivity.this, + message, + Toast.LENGTH_SHORT).show(); + } + }); + } + + private boolean isRecordPermissionGranted() { + return (ActivityCompat.checkSelfPermission(this, + Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED); + } + + private void requestRecordPermission(){ + ActivityCompat.requestPermissions( + this, + new String[]{Manifest.permission.RECORD_AUDIO}, + MY_PERMISSIONS_REQUEST_RECORD_AUDIO); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, + @NonNull int[] grantResults) { + + if (MY_PERMISSIONS_REQUEST_RECORD_AUDIO != requestCode) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + return; + } + + if (grantResults.length != 1 || + grantResults[0] != PackageManager.PERMISSION_GRANTED) { + + Toast.makeText(getApplicationContext(), + getString(R.string.need_record_audio_permission), + Toast.LENGTH_SHORT) + .show(); + } else { + // Permission was granted + beginTestThatRequiresRecording(); + } + } + + /** + * If needed, request recording permission before running test. + */ + protected void launchTestThatDoesRecording(Class clazz) { + mTestClass = clazz; + if (isRecordPermissionGranted()) { + beginTestThatRequiresRecording(); + } else { + requestRecordPermission(); + } + } + + protected void launchTestActivity(Class clazz) { + Intent intent = new Intent(this, clazz); + startActivity(intent); + } + + private void beginTestThatRequiresRecording() { + launchTestActivity(mTestClass); + } + +} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/BufferSizeView.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/BufferSizeView.java new file mode 100644 index 00000000..da7e6a2f --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/BufferSizeView.java @@ -0,0 +1,222 @@ +/* + * 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. + */ + +package com.mobileer.oboetester; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.LinearLayout; +import android.widget.RadioButton; +import android.widget.RadioGroup; +import android.widget.SeekBar; +import android.widget.TextView; + +public class BufferSizeView extends LinearLayout { + private OboeAudioStream mStream; + + private static final int FADER_THRESHOLD_MAX = 1000; // must match layout + private static final int USE_FADER = -1; + private static final int DEFAULT_NUM_BURSTS = 2; + private TextView mTextLabel; + private SeekBar mFader; + private ExponentialTaper mTaper; + private RadioGroup mBufferSizeGroup; + private RadioButton mBufferSizeRadio1; + private RadioButton mBufferSizeRadio2; + private RadioButton mBufferSizeRadio3; + private int mCachedCapacity; + private int mFramesPerBurst; + private int mNumBursts; + + private SeekBar.OnSeekBarChangeListener mFaderListener = new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + mNumBursts = USE_FADER; + updateRadioButtons(); + setBufferSizeByPosition(progress); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + } + }; + + public BufferSizeView(Context context) { + super(context); + initializeViews(context); + } + + public BufferSizeView(Context context, AttributeSet attrs) { + super(context, attrs); + initializeViews(context); + } + + public BufferSizeView(Context context, + AttributeSet attrs, + int defStyle) { + super(context, attrs, defStyle); + initializeViews(context); + } + + void setFaderNormalizedProgress(double fraction) { + mFader.setProgress((int) (fraction * FADER_THRESHOLD_MAX)); + } + + /** + * Inflates the views in the layout. + * + * @param context the current context for the view. + */ + private void initializeViews(Context context) { + LayoutInflater inflater = (LayoutInflater) context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.buffer_size_view, this); + + mTextLabel = (TextView) findViewById(R.id.textThreshold); + mFader = (SeekBar) findViewById(R.id.faderBufferSize); + mFader.setOnSeekBarChangeListener(mFaderListener); + mTaper = new ExponentialTaper(0.0, 1.0, 10.0); + mFader.setProgress(0); + + mBufferSizeGroup = (RadioGroup) findViewById(R.id.bufferSizeGroup); + + mBufferSizeRadio1 = (RadioButton) findViewById(R.id.bufferSize1); + mBufferSizeRadio1.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + onSizeRadioButtonClicked(view, 1); + } + }); + mBufferSizeRadio2 = (RadioButton) findViewById(R.id.bufferSize2); + mBufferSizeRadio2.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + onSizeRadioButtonClicked(view, 2); + } + }); + mBufferSizeRadio3 = (RadioButton) findViewById(R.id.bufferSize3); + mBufferSizeRadio3.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + onSizeRadioButtonClicked(view, 3); + } + }); + + mNumBursts = DEFAULT_NUM_BURSTS; + updateRadioButtons(); + updateBufferSize(); + } + + public void updateRadioButtons() { + if (mNumBursts == USE_FADER && mBufferSizeGroup != null) { + // Clear all the radio buttons using the group. + // If you clear a checked button directly then it stops working. + mBufferSizeGroup.clearCheck(); + } else if (mBufferSizeRadio3 != null) { + mBufferSizeRadio1.setChecked(mNumBursts == 1); + mBufferSizeRadio2.setChecked(mNumBursts == 2); + mBufferSizeRadio3.setChecked(mNumBursts == 3); + } + } + + private void onSizeRadioButtonClicked(View view, int numBursts) { + boolean checked = ((RadioButton) view).isChecked(); + if (!checked) return; + mNumBursts = numBursts; + setBufferSizeByNumBursts(numBursts); + } + + // sets mStream, mCachedCapacity and mFramesPerBurst + public void onStreamOpened(OboeAudioStream stream) { + mStream = stream; + if (mStream != null) { + int capacity = mStream.getBufferCapacityInFrames(); + if (capacity > 0) mCachedCapacity = capacity; + int framesPerBurst = mStream.getFramesPerBurst(); + if (framesPerBurst > 0) mFramesPerBurst = framesPerBurst; + } + updateBufferSize(); + } + + private void setBufferSizeByNumBursts(int numBursts) { + int sizeFrames = -1; + if (mStream != null) { + int framesPerBurst = mStream.getFramesPerBurst(); + if (framesPerBurst > 0) { + sizeFrames = numBursts * framesPerBurst; + } + } + StringBuffer message = new StringBuffer(); + message.append("bufferSize = #" + numBursts); + + setBufferSize(message, sizeFrames); + } + + private void setBufferSizeByPosition(int progress) { + int sizeFrames = -1; + double normalizedThreshold = 0.0; + + StringBuffer message = new StringBuffer(); + + normalizedThreshold = mTaper.linearToExponential( + ((double) progress) / FADER_THRESHOLD_MAX); + if (normalizedThreshold < 0.0) normalizedThreshold = 0.0; + else if (normalizedThreshold > 1.0) normalizedThreshold = 1.0; + int percent = (int) (normalizedThreshold * 100); + message.append("bufferSize = " + percent + "%"); + + if (mCachedCapacity > 0) { + sizeFrames = (int) (normalizedThreshold * mCachedCapacity); + } + setBufferSize(message, sizeFrames); + } + + private void setBufferSize(StringBuffer message, int sizeFrames) { + if (mStream != null) { + message.append(", " + sizeFrames); + if (mStream != null && sizeFrames >= 0) { + mStream.setBufferSizeInFrames(sizeFrames); + } + int bufferSize = mStream.getBufferSizeInFrames(); + if (bufferSize >= 0) { + message.append(" / " + bufferSize); + } + message.append(" / " + mCachedCapacity); + } + mTextLabel.setText(message.toString()); + } + + private void updateBufferSize() { + if (mNumBursts == USE_FADER) { + int progress = mFader.getProgress(); + setBufferSizeByPosition(progress); + } else { + setBufferSizeByNumBursts(mNumBursts); + } + } + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + mFader.setEnabled(enabled); + } +} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/CachedTextViewLog.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/CachedTextViewLog.java new file mode 100644 index 00000000..0cc2f8d9 --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/CachedTextViewLog.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 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. + */ + +package com.mobileer.oboetester; + +import android.app.Activity; +import android.widget.TextView; + +/** Wrap a TextView with a buffer and only update it periodically. + */ +public class CachedTextViewLog { + private final Activity mActivity; + private TextView mTextView; + private int mUpdatePeriodMillis = 500; + private long mLastUpdateTime; + private StringBuffer mBuffer = new StringBuffer(); + + public CachedTextViewLog(Activity activity, TextView textView) { + mActivity = activity; + mTextView = textView; + } + + public synchronized void append(String text) { + mBuffer.append(text); + long now = System.currentTimeMillis(); + if ((now - mLastUpdateTime) > mUpdatePeriodMillis) { + flush_l(); + } + } + + public synchronized void flush() { + flush_l(); + } + + // This must be called from a synchronized method. + private void flush_l() { + final String textToAdd = mBuffer.toString(); + mBuffer.setLength(0); + mLastUpdateTime = System.currentTimeMillis(); + mActivity.runOnUiThread(new Runnable() { + @Override + public void run() { + mTextView.append(textToAdd); + } + }); + } + + public synchronized void clear() { + mBuffer.setLength(0); + mLastUpdateTime = System.currentTimeMillis(); + mActivity.runOnUiThread(new Runnable() { + @Override + public void run() { + mTextView.setText(""); + } + }); + } + +} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/CircularCaptureBuffer.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/CircularCaptureBuffer.java similarity index 98% rename from mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/CircularCaptureBuffer.java rename to mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/CircularCaptureBuffer.java index bcc2d470..0e1f39ca 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/CircularCaptureBuffer.java +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/CircularCaptureBuffer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.sample.oboe.manualtest; +package com.mobileer.oboetester; /** diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/CommunicationDeviceView.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/CommunicationDeviceView.java new file mode 100644 index 00000000..870d5cd6 --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/CommunicationDeviceView.java @@ -0,0 +1,211 @@ +/* + * 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. + */ + +package com.mobileer.oboetester; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.media.AudioDeviceInfo; +import android.media.AudioManager; +import android.os.Build; +import android.util.AttributeSet; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.AdapterView; +import android.widget.CheckBox; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.mobileer.audio_device.CommunicationDeviceSpinner; + +public class CommunicationDeviceView extends LinearLayout { + + private AudioManager mAudioManager; + private CheckBox mSpeakerphoneCheckbox; + private CheckBox mScoCheckbox; + private TextView mSpeakerStatusView; + private TextView mScoStatusView; + private BroadcastReceiver mScoStateReceiver; + private boolean mScoStateReceiverRegistered = false; + private CommunicationDeviceSpinner mDeviceSpinner; + private int mScoState; + private CommDeviceSniffer mCommDeviceSniffer = new CommDeviceSniffer();; + + protected class CommDeviceSniffer extends NativeSniffer { + @Override + public void updateStatusText() { + showCommDeviceStatus(); + } + } + + public CommunicationDeviceView(Context context) { + super(context); + initializeViews(context); + } + + public CommunicationDeviceView(Context context, AttributeSet attrs) { + super(context, attrs); + initializeViews(context); + } + + public CommunicationDeviceView(Context context, + AttributeSet attrs, + int defStyle) { + super(context, attrs, defStyle); + initializeViews(context); + } + + /** + * Inflates the views in the layout. + * + * @param context the current context for the view. + */ + private void initializeViews(Context context) { + LayoutInflater inflater = (LayoutInflater) context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.comm_device_view, this); + + mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + mSpeakerphoneCheckbox = (CheckBox) findViewById(R.id.setSpeakerphoneOn); + mSpeakerphoneCheckbox.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + onSetSpeakerphoneOn(view); + } + }); + mSpeakerStatusView = (TextView) findViewById(R.id.spkr_status_view); + + mScoCheckbox = (CheckBox) findViewById(R.id.setBluetoothScoOn); + mScoCheckbox.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + onStartStopBluetoothSco(view); + } + }); + mScoStatusView = (TextView) findViewById(R.id.sco_status_view); + mScoStateReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + mScoState = intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, -1); + showCommDeviceStatus(); + } + }; + + mDeviceSpinner = (CommunicationDeviceSpinner) findViewById(R.id.comm_devices_spinner); + mDeviceSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + public void onItemSelected(AdapterView parent, View view, int position, long id) + { + AudioDeviceInfo[] commDeviceArray = mDeviceSpinner.getCommunicationsDevices(); + if (commDeviceArray != null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + if (position == CommunicationDeviceSpinner.POS_CLEAR) { + mAudioManager.clearCommunicationDevice(); + } else { + AudioDeviceInfo selectedDevice = commDeviceArray[position - CommunicationDeviceSpinner.POS_DEVICES]; // skip "Clear" + mAudioManager.setCommunicationDevice(selectedDevice); + } + } + } + showCommDeviceStatus(); + } + public void onNothingSelected(AdapterView parent) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + mAudioManager.clearCommunicationDevice(); + } + showCommDeviceStatus(); + } + }); + + showCommDeviceStatus(); + } + + public void onStart() { + registerScoStateReceiver(); + mCommDeviceSniffer.startSniffer(); + } + + + public void onStop() { + mCommDeviceSniffer.stopSniffer(); + mSpeakerphoneCheckbox.setChecked(false); + setSpeakerPhoneOn(false); + mScoCheckbox.setChecked(false); + mAudioManager.stopBluetoothSco(); + unregisterScoStateReceiver(); + } + + public void onSetSpeakerphoneOn(View view) { + Log.d(TestAudioActivity.TAG, "onSetSpeakerphoneOn() called from Checkbox"); + CheckBox checkBox = (CheckBox) view; + boolean enabled = checkBox.isChecked(); + setSpeakerPhoneOn(enabled); + showCommDeviceStatus(); + } + + private void setSpeakerPhoneOn(boolean enabled) { + Log.d(TestAudioActivity.TAG, "call setSpeakerphoneOn(" + enabled + ")"); + mAudioManager.setSpeakerphoneOn(enabled); + } + + private void showCommDeviceStatus() { + boolean enabled = mAudioManager.isSpeakerphoneOn(); + String text = ":" + (enabled ? "ON" : "OFF"); + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { + AudioDeviceInfo commDeviceInfo = mAudioManager.getCommunicationDevice(); + if (commDeviceInfo != null) { + text += ", #" + commDeviceInfo.getId(); + } + } + mSpeakerStatusView.setText(text + ","); + + if (mScoState == AudioManager.SCO_AUDIO_STATE_CONNECTING) { + text = ":WAIT"; + } else if (mScoState == AudioManager.SCO_AUDIO_STATE_CONNECTED) { + text = ":CON"; + } else if (mScoState == AudioManager.SCO_AUDIO_STATE_DISCONNECTED) { + text = ":DISCON"; + } + mScoStatusView.setText(text); + } + + public void onStartStopBluetoothSco(View view) { + CheckBox checkBox = (CheckBox) view; + if (checkBox.isChecked()) { + mAudioManager.startBluetoothSco(); + } else { + mAudioManager.stopBluetoothSco(); + } + } + + private synchronized void registerScoStateReceiver() { + if (!mScoStateReceiverRegistered) { + getContext().registerReceiver(mScoStateReceiver, + new IntentFilter(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED)); + mScoStateReceiverRegistered = true; + } + } + + private synchronized void unregisterScoStateReceiver() { + if (mScoStateReceiverRegistered) { + getContext().unregisterReceiver(mScoStateReceiver); + mScoStateReceiverRegistered = false; + } + } + +} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/DeviceReportActivity.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/DeviceReportActivity.java new file mode 100644 index 00000000..990ff86d --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/DeviceReportActivity.java @@ -0,0 +1,347 @@ +/* + * 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. + */ + +package com.mobileer.oboetester; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.Intent; +import android.hardware.usb.UsbDevice; +import android.hardware.usb.UsbManager; +import android.media.AudioDeviceCallback; +import android.media.AudioDeviceInfo; +import android.media.AudioManager; +import android.media.MediaCodecInfo; +import android.media.MediaCodecList; +import android.media.MicrophoneInfo; +import android.media.midi.MidiDeviceInfo; +import android.media.midi.MidiManager; +import android.os.Build; +import android.os.Bundle; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.appcompat.app.AppCompatActivity; + +import com.mobileer.audio_device.AudioDeviceInfoConverter; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Set; + +/** + * Print a report of all the available audio devices. + */ +public class DeviceReportActivity extends AppCompatActivity { + + class MyAudioDeviceCallback extends AudioDeviceCallback { + private HashMap mDevices + = new HashMap(); + + @Override + public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) { + for (AudioDeviceInfo info : addedDevices) { + mDevices.put(info.getId(), info); + } + reportDeviceInfo(mDevices.values()); + } + + public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) { + for (AudioDeviceInfo info : removedDevices) { + mDevices.remove(info.getId()); + } + reportDeviceInfo(mDevices.values()); + } + } + + MyAudioDeviceCallback mDeviceCallback = new MyAudioDeviceCallback(); + private TextView mAutoTextView; + private AudioManager mAudioManager; + private UsbManager mUsbManager; + private MidiManager mMidiManager; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_device_report); + mAutoTextView = (TextView) findViewById(R.id.text_log_device_report); + mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + mUsbManager = (UsbManager) getSystemService(Context.USB_SERVICE); + mMidiManager = (MidiManager) getSystemService(Context.MIDI_SERVICE); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.menu_main, menu); + MenuItem settings = menu.findItem(R.id.action_share); + settings.setOnMenuItemClickListener(item -> { + if(mAutoTextView !=null) { + Intent sendIntent = new Intent(); + sendIntent.setAction(Intent.ACTION_SEND); + sendIntent.putExtra(Intent.EXTRA_TEXT, mAutoTextView.getText().toString()); + sendIntent.setType("text/plain"); + Intent shareIntent = Intent.createChooser(sendIntent, null); + startActivity(shareIntent); + } + return false; + }); + return true; + } + + @Override + protected void onStart() { + super.onStart(); + addAudioDeviceCallback(); + } + + @Override + protected void onStop() { + removeAudioDeviceCallback(); + super.onStop(); + } + + @TargetApi(23) + private void addAudioDeviceCallback(){ + // Note that we will immediately receive a call to onDevicesAdded with the list of + // devices which are currently connected. + mAudioManager.registerAudioDeviceCallback(mDeviceCallback, null); + } + + @TargetApi(23) + private void removeAudioDeviceCallback(){ + mAudioManager.unregisterAudioDeviceCallback(mDeviceCallback); + } + + private void reportDeviceInfo(Collection devices) { + logClear(); + StringBuffer report = new StringBuffer(); + report.append("Device Report:\n"); + report.append("App: ").append(MainActivity.getVersionText()).append("\n"); + report.append("Device: ").append(Build.MANUFACTURER).append(", ").append(Build.MODEL) + .append(", ").append(Build.PRODUCT).append("\n"); + + report.append(reportExtraDeviceInfo()); + report.append("\n"); + + for (AudioDeviceInfo deviceInfo : devices) { + report.append("\n==== Device =================== " + deviceInfo.getId() + "\n"); + String item = AudioDeviceInfoConverter.toString(deviceInfo); + report.append(item); + } + report.append(reportAllMicrophones()); + report.append(reportUsbDevices()); + report.append(reportMidiDevices()); + report.append(reportMediaCodecs()); + log(report.toString()); + } + + public String reportUsbDevices() { + StringBuffer report = new StringBuffer(); + report.append("\n############################"); + report.append("\nUsb Device Report:\n"); + try { + HashMap usbDeviceList = mUsbManager.getDeviceList(); + for (UsbDevice usbDevice : usbDeviceList.values()) { + report.append("\n==== USB Device ========= " + usbDevice.getDeviceId()); + report.append("\nProduct Name : " + usbDevice.getProductName()); + report.append("\nProduct ID : 0x" + Integer.toHexString(usbDevice.getProductId())); + report.append("\nManufacturer Name : " + usbDevice.getManufacturerName()); + report.append("\nVendor ID : 0x" + Integer.toHexString(usbDevice.getVendorId())); + report.append("\nDevice Name : " + usbDevice.getDeviceName()); + report.append("\nDevice Protocol : " + usbDevice.getDeviceProtocol()); + report.append("\nDevice Class : " + usbDevice.getDeviceClass()); + report.append("\nDevice Subclass : " + usbDevice.getDeviceSubclass()); + report.append("\nVersion : " + usbDevice.getVersion()); + report.append("\n" + usbDevice); + report.append("\n"); + } + } catch (Exception e) { + Log.e(TestAudioActivity.TAG, "Caught ", e); + showErrorToast(e.getMessage()); + report.append("\nERROR: " + e.getMessage() + "\n"); + } + return report.toString(); + } + + public String reportMidiDevices() { + StringBuffer report = new StringBuffer(); + report.append("\n############################"); + report.append("\nMidi Device Report:\n"); + try { + MidiDeviceInfo[] midiDeviceInfos = mMidiManager.getDevices(); + for (MidiDeviceInfo midiDeviceInfo : midiDeviceInfos) { + report.append("\n==== MIDI Device ========= " + midiDeviceInfo.getId()); + addMidiDeviceInfoToDeviceReport(midiDeviceInfo, report); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + Set umpDeviceInfos = + mMidiManager.getDevicesForTransport(MidiManager.TRANSPORT_UNIVERSAL_MIDI_PACKETS); + for (MidiDeviceInfo midiDeviceInfo : umpDeviceInfos) { + report.append("\n==== UMP Device ========= " + midiDeviceInfo.getId()); + addMidiDeviceInfoToDeviceReport(midiDeviceInfo, report); + } + } + } catch (Exception e) { + Log.e(TestAudioActivity.TAG, "Caught ", e); + showErrorToast(e.getMessage()); + report.append("\nERROR: " + e.getMessage() + "\n"); + } + return report.toString(); + } + + private void addMidiDeviceInfoToDeviceReport(MidiDeviceInfo midiDeviceInfo, + StringBuffer report){ + report.append("\nInput Count : " + midiDeviceInfo.getInputPortCount()); + report.append("\nOutput Count : " + midiDeviceInfo.getOutputPortCount()); + report.append("\nType : " + midiDeviceInfo.getType()); + report.append("\nIs Private : " + midiDeviceInfo.isPrivate()); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + report.append("\nDefault Protocol : " + midiDeviceInfo.getDefaultProtocol()); + } + report.append("\n" + midiDeviceInfo); + report.append("\n"); + } + + public String reportAllMicrophones() { + StringBuffer report = new StringBuffer(); + report.append("\n############################"); + report.append("\nMicrophone Report:\n"); + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) { + try { + List micList = mAudioManager.getMicrophones(); + for (MicrophoneInfo micInfo : micList) { + String micItem = MicrophoneInfoConverter.reportMicrophoneInfo(micInfo); + report.append(micItem); + } + } catch (IOException e) { + Log.e(TestAudioActivity.TAG, "Caught ", e); + return e.getMessage(); + } catch (Exception e) { + Log.e(TestAudioActivity.TAG, "Caught ", e); + showErrorToast(e.getMessage()); + report.append("\nERROR: " + e.getMessage() + "\n"); + } + } else { + report.append("\nMicrophoneInfo not available on V" + android.os.Build.VERSION.SDK_INT); + } + return report.toString(); + } + + private String reportExtraDeviceInfo() { + StringBuffer report = new StringBuffer(); + report.append("\n\n############################"); + report.append("\nAudioManager:"); + report.append(AudioQueryTools.getAudioManagerReport(mAudioManager)); + report.append("\n\nFeatures:"); + report.append(AudioQueryTools.getAudioFeatureReport(getPackageManager())); + report.append(AudioQueryTools.getMediaPerformanceClass()); + report.append("\n\nProperties:"); + report.append(AudioQueryTools.getAudioPropertyReport()); + return report.toString(); + } + + public String reportMediaCodecs() { + StringBuffer report = new StringBuffer(); + report.append("\n############################"); + report.append("\nMedia Codec Device Report:\n"); + try { + MediaCodecList mediaCodecList = new MediaCodecList(MediaCodecList.REGULAR_CODECS); + MediaCodecInfo[] mediaCodecInfos = mediaCodecList.getCodecInfos(); + for (MediaCodecInfo mediaCodecInfo : mediaCodecInfos) { + report.append("\n==== MediaCodecInfo ========= " + mediaCodecInfo.getName()); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + report.append("\nCanonical Name : " + mediaCodecInfo.getCanonicalName()); + report.append("\nIs Alias : " + mediaCodecInfo.isAlias()); + report.append("\nIs Hardware Accelerated: " + mediaCodecInfo.isHardwareAccelerated()); + report.append("\nIs Software Only : " + mediaCodecInfo.isSoftwareOnly()); + report.append("\nIs Vendor : " + mediaCodecInfo.isVendor()); + } + report.append("\nIs Encoder : " + mediaCodecInfo.isEncoder()); + report.append("\nSupported Types : " + Arrays.toString(mediaCodecInfo.getSupportedTypes())); + for(String type : mediaCodecInfo.getSupportedTypes()) { + MediaCodecInfo.CodecCapabilities codecCapabilities = + mediaCodecInfo.getCapabilitiesForType(type); + MediaCodecInfo.AudioCapabilities audioCapabilities = + codecCapabilities.getAudioCapabilities(); + if (audioCapabilities != null) { + report.append("\nAudio Type: " + type); + report.append("\nBitrate Range: " + audioCapabilities.getBitrateRange()); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + report.append("\nInput Channel Count Ranges: " + Arrays.toString(audioCapabilities.getInputChannelCountRanges())); + report.append("\nMin Input Channel Count: " + audioCapabilities.getMinInputChannelCount()); + } + report.append("\nMax Input Channel Count: " + audioCapabilities.getMaxInputChannelCount()); + report.append("\nSupported Sample Rate Ranges: " + Arrays.toString(audioCapabilities.getSupportedSampleRateRanges())); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + // Avoid bug b/122116282 + report.append("\nSupported Sample Rates: " + Arrays.toString(audioCapabilities.getSupportedSampleRates())); + } + } + report.append("\nIs Encoder : " + mediaCodecInfo.isEncoder()); + } + report.append("\n"); + } + } catch (Exception e) { + Log.e(TestAudioActivity.TAG, "Caught ", e); + showErrorToast(e.getMessage()); + report.append("\nERROR: " + e.getMessage() + "\n"); + } + return report.toString(); + } + + // Write to scrollable TextView + private void log(final String text) { + runOnUiThread(new Runnable() { + @Override + public void run() { + mAutoTextView.append(text); + mAutoTextView.append("\n"); + } + }); + } + + private void logClear() { + runOnUiThread(new Runnable() { + @Override + public void run() { + mAutoTextView.setText(""); + } + }); + } + + protected void showErrorToast(String message) { + String text = "Error: " + message; + Log.e(TestAudioActivity.TAG, text); + showToast(text); + } + + protected void showToast(final String message) { + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(DeviceReportActivity.this, + message, + Toast.LENGTH_SHORT).show(); + } + }); + } +} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/DoubleStatistics.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/DoubleStatistics.java new file mode 100644 index 00000000..af7f86d3 --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/DoubleStatistics.java @@ -0,0 +1,68 @@ +/* + * 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. + */ + +package com.mobileer.oboetester; + +import java.util.ArrayList; + +class DoubleStatistics { + ArrayList mValues = new ArrayList(); + private double mMin = Double.MAX_VALUE; + private double mMax = Double.MIN_VALUE; + private double mSum = 0.0; + + // Number of measurements. + public int count() { + return mValues.size(); + } + + public void add(double value) { + mValues.add(value); + mMin = Math.min(value, mMin); + mMax = Math.max(value, mMax); + mSum += value; + } + + public double calculateMeanAbsoluteDeviation(double mean) { + double deviationSum = 0.0; + for (double value : mValues) { + deviationSum += Math.abs(value - mean); + } + return deviationSum / mValues.size(); + } + + // This will crash if there are no values added. + public double calculateMean() { + return mSum / mValues.size(); + } + + public double getMin() { + return mMin; + } + + public double getMax() { + return mMax; + } + + public double getSum() { + return mSum; + } + + // This will crash if there are no values added. + public double getLast() { + return mValues.get(mValues.size() - 1); + } +} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/DynamicWorkloadActivity.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/DynamicWorkloadActivity.java new file mode 100644 index 00000000..9340100e --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/DynamicWorkloadActivity.java @@ -0,0 +1,473 @@ +/* + * Copyright 2015 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. + */ + +package com.mobileer.oboetester; + +import android.graphics.Color; +import android.graphics.Typeface; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.view.View; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.LinearLayout; +import android.widget.TextView; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Locale; + +/** + * Demonstrate the behavior of a changing CPU load on underruns. + * Display the workload and the callback duration in a chart. + * Enable or disable PerformanceHints (ADPF) using a checkbox. + * This might boost the CPU frequency when Oboe is taking too long to compute the next buffer. + * ADPF docs at: https://developer.android.com/reference/android/os/PerformanceHintManager + */ +public class DynamicWorkloadActivity extends TestOutputActivityBase { + private static final int WORKLOAD_HIGH_MIN = 30; + private static final int WORKLOAD_HIGH_MAX = 150; + // When the CPU is completely saturated then the load will be above 1.0. + public static final double LOAD_RECOVERY_HIGH = 1.0; + // Use a slightly lower value for going low so that the comparator has hysteresis. + public static final double LOAD_RECOVERY_LOW = 0.95; + + private static final float MARGIN_ABOVE_WORKLOAD_FOR_CPU = 1.2f; + + // By default, set high workload to 70 voices, which is reasonable for most devices. + public static final double WORKLOAD_PROGRESS_FOR_70_VOICES = 0.53; + + public static final String KEY_USE_ADPF = "use_adpf"; + public static final boolean VALUE_DEFAULT_USE_ADPF = false; + public static final String KEY_USE_WORKLOAD = "use_workload"; + public static final boolean VALUE_DEFAULT_USE_WORKLOAD = false; + public static final String KEY_SCROLL_GRAPHICS = "scroll_graphics"; + public static final boolean VALUE_DEFAULT_SCROLL_GRAPHICS = false; + + private Button mStopButton; + private Button mStartButton; + private TextView mResultView; + private LinearLayout mAffinityLayout; + private ArrayList mAffinityBoxes = new ArrayList(); + private WorkloadUpdateThread mUpdateThread; + + private MultiLineChart mMultiLineChart; + private MultiLineChart.Trace mMaxCpuLoadTrace; + private MultiLineChart.Trace mWorkloadTrace; + private CheckBox mUseAltAdpfBox; + private CheckBox mPerfHintBox; + private CheckBox mWorkloadReportBox; + private boolean mDrawChartAlways = true; + private CheckBox mDrawAlwaysBox; + private int mCpuCount; + private boolean mShouldUseADPF; + private boolean mShouldUseWorkloadReporting; + + private static final int WORKLOAD_LOW = 1; + private int mWorkloadHigh; // this will get set later + private WorkloadView mDynamicWorkloadView; + + // Periodically query the status of the streams. + protected class WorkloadUpdateThread { + public static final int SNIFFER_UPDATE_PERIOD_MSEC = 40; + public static final int SNIFFER_UPDATE_DELAY_MSEC = 300; + public static final int SNIFFER_TOGGLE_PERIOD_MSEC = 3000; + private static final int STATE_IDLE = 0; + private static final int STATE_RUN_LOW = 1; + private static final int STATE_RUN_HIGH = 2; + + private Handler mHandler; + + private int mWorkloadCurrent = 1; + + private int mState = STATE_IDLE; + private long mLastToggleTime = 0; + private long mRecoveryTimeBegin; + private long mRecoveryTimeEnd; + private long mStartTimeNanos; + + String stateToString(int state) { + switch(state) { + case STATE_IDLE: + return "Idle"; + case STATE_RUN_LOW: + return "low"; + case STATE_RUN_HIGH: + return "HIGH"; + default: + return "Unrecognized"; + } + } + + // Display status info for the stream. + private Runnable runnableCode = new Runnable() { + @Override + public void run() { + int nextWorkload = mWorkloadCurrent; + AudioStreamBase stream = mAudioOutTester.getCurrentAudioStream(); + float cpuLoad = stream.getCpuLoad(); + float maxCpuLoad = stream.getAndResetMaxCpuLoad(); + int cpuMask = stream.getAndResetCpuMask(); + long now = System.currentTimeMillis(); + boolean drawChartOnce = false; + + switch (mState) { + case STATE_IDLE: + drawChartOnce = true; // clear old chart + mState = STATE_RUN_LOW; + mLastToggleTime = now; + break; + case STATE_RUN_LOW: + nextWorkload = WORKLOAD_LOW; + if ((now - mLastToggleTime) > SNIFFER_TOGGLE_PERIOD_MSEC) { + mLastToggleTime = now; + mState = STATE_RUN_HIGH; + mRecoveryTimeBegin = 0; + mRecoveryTimeEnd = 0; + } + break; + case STATE_RUN_HIGH: + nextWorkload = mWorkloadHigh; + if ((now - mLastToggleTime) > SNIFFER_TOGGLE_PERIOD_MSEC) { + mLastToggleTime = now; + mState = STATE_RUN_LOW; + // Draw now when a CPU spike will not affect the result. + drawChartOnce = true; + } + + if (mRecoveryTimeBegin == 0) { + if (maxCpuLoad > LOAD_RECOVERY_HIGH) { + mRecoveryTimeBegin = now; + } + } else if (mRecoveryTimeEnd == 0) { + if (maxCpuLoad < LOAD_RECOVERY_LOW) { + mRecoveryTimeEnd = now; + } + } else if (maxCpuLoad > LOAD_RECOVERY_LOW) { + mRecoveryTimeEnd = now; + } + break; + } + stream.setWorkload((int) nextWorkload); + mWorkloadCurrent = nextWorkload; + // Update chart + float nowMicros = (System.nanoTime() - mStartTimeNanos) * 0.001f; + mMultiLineChart.addX(nowMicros); + mMaxCpuLoadTrace.add((float) maxCpuLoad); + mWorkloadTrace.add((float) mWorkloadCurrent); + if (drawChartOnce || mDrawChartAlways){ + mMultiLineChart.update(); + } + + // Display numbers + String recoveryTimeString = (mRecoveryTimeEnd <= mRecoveryTimeBegin) ? + "---" : ((mRecoveryTimeEnd - mRecoveryTimeBegin) + " msec"); + String message = + "#Voices = " + (int) nextWorkload + + "\nWorkState = " + stateToString(mState) + + "\nCPU = " + String.format(Locale.getDefault(), "%6.3f%c", cpuLoad * 100, '%') + + "\ncores = " + cpuMaskToString(cpuMask, mCpuCount) + + "\nRecovery = " + recoveryTimeString; + postResult(message); + + mHandler.postDelayed(runnableCode, SNIFFER_UPDATE_PERIOD_MSEC); + } + }; + + private void start() { + stop(); + mStartTimeNanos = System.nanoTime(); + mMultiLineChart.reset(); + mState = STATE_IDLE; + mHandler = new Handler(Looper.getMainLooper()); + // Start the initial runnable task by posting through the handler + mHandler.postDelayed(runnableCode, SNIFFER_UPDATE_DELAY_MSEC); + } + + private void stop() { + if (mHandler != null) { + mHandler.removeCallbacks(runnableCode); + } + } + + } + + private void setWorkloadHigh(int workloadHigh) { + mWorkloadHigh = workloadHigh; + } + + + /** + * This text will look best in a monospace font. + * @param cpuMask CPU core bit mask + * @return a text display of the selected cores like "--2-45-7" + */ + // TODO move this to some utility class + private String cpuMaskToString(int cpuMask, int cpuCount) { + String text = ""; + long longMask = ((long) cpuMask) & 0x0FFFFFFFFL; + int index = 0; + while (longMask != 0 || index < cpuCount) { + text += ((longMask & 1) != 0) ? hexDigit(index) : "-"; + longMask = longMask >> 1; + index++; + } + return text; + } + + private char hexDigit(int n) { + byte x = (byte)(n & 0x0F); + if (x < 10) return (char)('0' + x); + else return (char)('A' + x); + } + + @Override + protected void inflateActivity() { + setContentView(R.layout.activity_dynamic_workload); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mAudioOutTester = addAudioOutputTester(); + + mResultView = (TextView) findViewById(R.id.resultView); + mResultView.setTypeface(Typeface.MONOSPACE); + mStartButton = (Button) findViewById(R.id.button_start); + mStopButton = (Button) findViewById(R.id.button_stop); + + mDynamicWorkloadView = (WorkloadView) findViewById(R.id.dynamic_workload_view); + mWorkloadView.setVisibility(View.GONE); + + // Add a row of checkboxes for setting CPU affinity. + mCpuCount = NativeEngine.getCpuCount(); + final int defaultCpuAffinityMask = 0; + View.OnClickListener checkBoxListener = new View.OnClickListener() { + @Override + public void onClick(View view) { + // Create a mack from all the checkboxes. + int mask = 0; + for (int cpuIndex = 0; cpuIndex < mCpuCount; cpuIndex++) { + CheckBox checkBox = mAffinityBoxes.get(cpuIndex); + if (checkBox.isChecked()) { + mask |= (1 << cpuIndex); + } + } + NativeEngine.setCpuAffinityMask(mask); + } + }; + mAffinityLayout = (LinearLayout) findViewById(R.id.affinityLayout); + for (int cpuIndex = 0; cpuIndex < mCpuCount; cpuIndex++) { + CheckBox checkBox = new CheckBox(DynamicWorkloadActivity.this); + mAffinityLayout.addView(checkBox); + mAffinityBoxes.add(checkBox); + checkBox.setText(cpuIndex + ""); + checkBox.setOnClickListener(checkBoxListener); + if (((1 << cpuIndex) & defaultCpuAffinityMask) != 0) { + checkBox.setChecked(true); + } + } + NativeEngine.setCpuAffinityMask(defaultCpuAffinityMask); + + mMultiLineChart = (MultiLineChart) findViewById(R.id.multiline_chart); + mMaxCpuLoadTrace = mMultiLineChart.createTrace("CPU", Color.RED, + 0.0f, 2.0f); + mWorkloadTrace = mMultiLineChart.createTrace("Work", Color.BLUE, + 0.0f, (MARGIN_ABOVE_WORKLOAD_FOR_CPU * WORKLOAD_HIGH_MAX)); + + mPerfHintBox = (CheckBox) findViewById(R.id.enable_perf_hint); + mWorkloadReportBox = (CheckBox) findViewById(R.id.enable_workload_report); + + // TODO remove when finished with ADPF experiments. + mUseAltAdpfBox = (CheckBox) findViewById(R.id.use_alternative_adpf); + mUseAltAdpfBox.setOnClickListener(buttonView -> { + CheckBox checkBox = (CheckBox) buttonView; + setUseAlternativeAdpf(checkBox.isChecked()); + mPerfHintBox.setEnabled(!checkBox.isChecked()); + }); + mUseAltAdpfBox.setVisibility(View.GONE); + + mPerfHintBox.setOnClickListener(buttonView -> { + CheckBox checkBox = (CheckBox) buttonView; + mShouldUseADPF = checkBox.isChecked(); + setPerformanceHintEnabled(mShouldUseADPF); + mUseAltAdpfBox.setEnabled(!mShouldUseADPF); + mWorkloadReportBox.setEnabled(mShouldUseADPF); + }); + + mWorkloadReportBox.setOnClickListener(buttonView -> { + CheckBox checkBox = (CheckBox) buttonView; + mShouldUseWorkloadReporting = checkBox.isChecked(); + setWorkloadReportingEnabled(mShouldUseWorkloadReporting); + }); + mWorkloadReportBox.setEnabled(mShouldUseADPF); + + CheckBox hearWorkloadBox = (CheckBox) findViewById(R.id.hear_workload); + hearWorkloadBox.setOnClickListener(buttonView -> { + CheckBox checkBox = (CheckBox) buttonView; + setHearWorkload(checkBox.isChecked()); + }); + + mDrawAlwaysBox = (CheckBox) findViewById(R.id.draw_always); + mDrawAlwaysBox.setOnClickListener(buttonView -> { + CheckBox checkBox = (CheckBox) buttonView; + mDrawChartAlways = checkBox.isChecked(); + }); + + if (mDynamicWorkloadView != null) { + mDynamicWorkloadView.setWorkloadReceiver((w) -> { + setWorkloadHigh(w); + }); + + mDynamicWorkloadView.setLabel("High Workload"); + mDynamicWorkloadView.setRange(WORKLOAD_HIGH_MIN, WORKLOAD_HIGH_MAX); + mDynamicWorkloadView.setFaderNormalizedProgress(WORKLOAD_PROGRESS_FOR_70_VOICES); + } + + updateButtons(false); + updateEnabledWidgets(); + hideSettingsViews(); // make more room + } + + private void setHearWorkload(boolean checked) { + mAudioOutTester.getCurrentAudioStream().setHearWorkload(checked); + } + + private void setPerformanceHintEnabled(boolean checked) { + mAudioOutTester.getCurrentAudioStream().setPerformanceHintEnabled(checked); + } + + private void setWorkloadReportingEnabled(boolean enabled) { + NativeEngine.setWorkloadReportingEnabled(enabled); + } + + private void updateButtons(boolean running) { + mStartButton.setEnabled(!running); + mStopButton.setEnabled(running); + mPerfHintBox.setEnabled(running); + mWorkloadReportBox.setEnabled(running); + } + + private void postResult(final String text) { + runOnUiThread(new Runnable() { + public void run() { + mResultView.setText(text); + } + }); + } + + @Override + int getActivityType() { + return ACTIVITY_DYNAMIC_WORKLOAD; + } + + public void startTest(View view) { + startTest(); + } + + private void startTest() { + try { + openAudio(); + } catch (IOException e) { + e.printStackTrace(); + showErrorToast("Open audio failed!"); + return; + } + try { + super.startAudio(); + updateButtons(true); + postResult("Running test"); + mUpdateThread = new WorkloadUpdateThread(); + mUpdateThread.start(); + } catch (IOException e) { + e.printStackTrace(); + showErrorToast("Start audio failed! " + e.getMessage()); + return; + } + } + + public void stopTest(View view) { + onStopTest(); + } + + @Override + public void onStopTest() { + WorkloadUpdateThread updateThread = mUpdateThread; + if (updateThread != null) { + updateThread.stop(); + } + updateButtons(false); + super.onStopTest(); + } + + + @Override + public void startTestUsingBundle() { + try { + StreamConfiguration requestedOutConfig = mAudioOutTester.requestedConfiguration; + IntentBasedTestSupport.configureOutputStreamFromBundle(mBundleFromIntent, requestedOutConfig); + + // Specific options. + mShouldUseADPF = mBundleFromIntent.getBoolean(KEY_USE_ADPF, + VALUE_DEFAULT_USE_ADPF); + mShouldUseWorkloadReporting = mBundleFromIntent.getBoolean(KEY_USE_WORKLOAD, + VALUE_DEFAULT_USE_WORKLOAD); + mDrawChartAlways = + mBundleFromIntent.getBoolean(KEY_SCROLL_GRAPHICS, + VALUE_DEFAULT_SCROLL_GRAPHICS); + + startTest(); + + runOnUiThread(() -> { + mPerfHintBox.setChecked(mShouldUseADPF); + setPerformanceHintEnabled(mShouldUseADPF); + mWorkloadReportBox.setChecked(mShouldUseWorkloadReporting); + setWorkloadReportingEnabled(mShouldUseWorkloadReporting); + mDrawAlwaysBox.setChecked(mDrawChartAlways); + }); + + int durationSeconds = IntentBasedTestSupport.getDurationSeconds(mBundleFromIntent); + if (durationSeconds > 0) { + // Schedule the end of the test. + Handler handler = new Handler(Looper.getMainLooper()); // UI thread + handler.postDelayed(new Runnable() { + @Override + public void run() { + stopAutomaticTest(); + } + }, durationSeconds * 1000); + } + } catch (Exception e) { + showErrorToast(e.getMessage()); + } finally { + mBundleFromIntent = null; + } + } + + void stopAutomaticTest() { + String report = getCommonTestReport(); + AudioStreamBase outputStream =mAudioOutTester.getCurrentAudioStream(); + report += "out.xruns = " + outputStream.getXRunCount() + "\n"; + report += "use.adpf = " + (mShouldUseADPF ? "yes" : "no") + "\n"; + report += "use.workload = " + (mShouldUseWorkloadReporting ? "yes" : "no") + "\n"; + report += "scroll.graphics = " + (mDrawChartAlways ? "yes" : "no") + "\n"; + onStopTest(); + maybeWriteTestResult(report); + mTestRunningByIntent = false; + } +} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/EchoActivity.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/EchoActivity.java similarity index 63% rename from mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/EchoActivity.java rename to mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/EchoActivity.java index 688d1abb..b78f2b96 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/EchoActivity.java +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/EchoActivity.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.sample.oboe.manualtest; +package com.mobileer.oboetester; import android.os.Bundle; import android.view.View; @@ -40,6 +40,9 @@ public class EchoActivity extends TestInputActivity { private double mDelayTime; private Button mStartButton; private Button mStopButton; + private TextView mStatusTextView; + + private ColdStartSniffer mNativeSniffer = new ColdStartSniffer(); protected static final int MAX_DELAY_TIME_PROGRESS = 1000; @@ -58,6 +61,59 @@ public class EchoActivity extends TestInputActivity { } }; + // Periodically query for cold start latency from the native code until it is ready. + protected class ColdStartSniffer extends NativeSniffer { + + private int stableCallCount = 0; + private static final int STABLE_CALLS_NEEDED = 20; + private int mInputLatency; + private int mOutputLatency; + + @Override + public void startSniffer() { + stableCallCount = 0; + mInputLatency = -1; + mOutputLatency = -1; + super.startSniffer(); + } + + @Override + public boolean isComplete() { + mInputLatency = getColdStartInputMillis(); + mOutputLatency = getColdStartOutputMillis(); + if (mInputLatency > 0 && mOutputLatency > 0) { + stableCallCount++; + } + return stableCallCount > STABLE_CALLS_NEEDED; + } + + private String getCurrentStatusReport() { + StringBuffer message = new StringBuffer(); + message.append("cold.start.input.msec = " + + ((mInputLatency > 0) + ? mInputLatency + : "?") + + "\n"); + message.append("cold.start.output.msec = " + + ((mOutputLatency > 0) + ? mOutputLatency + : "?") + + "\n"); + message.append("stable.call.count = " + stableCallCount + "\n"); + return message.toString(); + } + + @Override + public void updateStatusText() { + String message = getCurrentStatusReport(); + mStatusTextView.setText(message); + } + + } + + private native int getColdStartInputMillis(); + private native int getColdStartOutputMillis(); + @Override protected void inflateActivity() { setContentView(R.layout.activity_echo); @@ -75,6 +131,8 @@ public class EchoActivity extends TestInputActivity { mStopButton = (Button) findViewById(R.id.button_stop_echo); mStopButton.setEnabled(false); + mStatusTextView = (TextView) findViewById(R.id.text_status); + mTextDelayTime = (TextView) findViewById(R.id.text_delay_time); mFaderDelayTime = (SeekBar) findViewById(R.id.fader_delay_time); mFaderDelayTime.setOnSeekBarChangeListener(mDelayListener); @@ -84,6 +142,7 @@ public class EchoActivity extends TestInputActivity { 100.0); mFaderDelayTime.setProgress(MAX_DELAY_TIME_PROGRESS / 2); + mCommunicationDeviceView = (CommunicationDeviceView) findViewById(R.id.comm_device_view); hideSettingsViews(); } @@ -96,10 +155,8 @@ public class EchoActivity extends TestInputActivity { private native void setDelayTime(double delayTimeSeconds); - @Override - protected void onStart() { - super.onStart(); - setActivityType(ACTIVITY_ECHO); + int getActivityType() { + return ACTIVITY_ECHO; } @Override @@ -116,12 +173,14 @@ public class EchoActivity extends TestInputActivity { mStartButton.setEnabled(false); mStopButton.setEnabled(true); keepScreenOn(true); + mNativeSniffer.startSniffer(); } catch (IOException e) { showErrorToast(e.getMessage()); } } public void onStopEcho(View view) { + mNativeSniffer.stopSniffer(); stopAudio(); closeAudio(); mStartButton.setEnabled(true); @@ -133,8 +192,4 @@ public class EchoActivity extends TestInputActivity { boolean isOutput() { return false; } - - @Override - public void setupEffects(int sessionId) { - } } diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/ExponentialTaper.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ExponentialTaper.java similarity index 97% rename from mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/ExponentialTaper.java rename to mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ExponentialTaper.java index 9867751f..a6d44959 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/ExponentialTaper.java +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ExponentialTaper.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.sample.oboe.manualtest; +package com.mobileer.oboetester; /** * Maps integer range info to a double value along an exponential scale. diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ExternalFileWriter.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ExternalFileWriter.java new file mode 100644 index 00000000..759690e4 --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ExternalFileWriter.java @@ -0,0 +1,55 @@ +/* + * 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. + */ + +package com.mobileer.oboetester; + +import android.content.Context; +import android.util.Log; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Writer; + +public class ExternalFileWriter { + private static final String TAG = "OboeTester"; + private Context mContext; + + public ExternalFileWriter(Context context) { + mContext = context; + } + + public File writeStringToExternalFile(String result, String fileName) throws IOException { + File dir = mContext.getExternalFilesDir(null); + File resultFile = new File(dir, fileName); + Log.d(TAG, "EXTFILE = " + resultFile.getAbsolutePath()); + Writer writer = null; + try { + writer = new OutputStreamWriter(new FileOutputStream(resultFile)); + writer.write(result); + } finally { + if (writer != null) { + try { + writer.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + return resultFile; + } +} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ExternalTapToToneActivity.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ExternalTapToToneActivity.java new file mode 100644 index 00000000..a08e5cef --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ExternalTapToToneActivity.java @@ -0,0 +1,98 @@ +package com.mobileer.oboetester; + +import android.os.Bundle; +import android.view.View; +import android.view.WindowManager; +import android.widget.Button; +import android.widget.Toast; + +import androidx.appcompat.app.AppCompatActivity; + +import java.io.IOException; + +/** + * Measure the tap-to-tone latency for other apps or devices. + */ +public class ExternalTapToToneActivity extends AppCompatActivity { + private static final String TAG = "OboeTester"; + + protected TapToToneTester mTapToToneTester; + private Button mStopButton; + private Button mStartButton; + private Button mAnalyzeButton; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_external_tap_to_tone); + + mTapToToneTester = new TapToToneTester(this, + getResources().getString(R.string.external_tap_instructions)); + + mStartButton = (Button) findViewById(R.id.button_start); + mStopButton = (Button) findViewById(R.id.button_stop); + mAnalyzeButton = (Button) findViewById(R.id.button_analyze); + updateButtons(false); + } + + private void updateButtons(boolean running) { + mStartButton.setEnabled(!running); + mAnalyzeButton.setEnabled(running); + mStopButton.setEnabled(running); + } + + public void analyseAndShowResults() { + TapToToneTester.TestResult result = mTapToToneTester.analyzeCapturedAudio(); + if (result != null) { + mTapToToneTester.showTestResults(result); + } + } + + public void analyze(View view) { + analyseAndShowResults(); + } + + public void startTest(View view) { + try { + mTapToToneTester.resetLatency(); + mTapToToneTester.start(); + updateButtons(true); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } catch (IOException e) { + e.printStackTrace(); + showErrorToast("Start audio failed! " + e.getMessage()); + return; + } + } + + public void stopTest(View view) { + mTapToToneTester.stop(); + updateButtons(false); + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + + @Override + public void onStop() { + mTapToToneTester.stop(); + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + super.onStop(); + } + + + protected void showErrorToast(String message) { + showToast("Error: " + message); + } + + protected void showToast(final String message) { + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(ExternalTapToToneActivity.this, + message, + Toast.LENGTH_SHORT).show(); + } + }); + } + + +} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ExtraTestsActivity.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ExtraTestsActivity.java new file mode 100644 index 00000000..12b9efb8 --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ExtraTestsActivity.java @@ -0,0 +1,45 @@ +package com.mobileer.oboetester; + +import android.os.Bundle; +import android.view.View; + +public class ExtraTestsActivity extends BaseOboeTesterActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_extra_tests); + } + + public void onLaunchMainActivity(View view) { + launchTestActivity(MainActivity.class); + } + + public void onLaunchExternalTapTest(View view) { + launchTestThatDoesRecording(ExternalTapToToneActivity.class); + } + + public void onLaunchPlugLatencyTest(View view) { + launchTestActivity(TestPlugLatencyActivity.class); + } + + public void onLaunchErrorCallbackTest(View view) { + launchTestActivity(TestErrorCallbackActivity.class); + } + + public void onLaunchRouteDuringCallbackTest(View view) { + launchTestThatDoesRecording(TestRouteDuringCallbackActivity.class); + } + + public void onLaunchDynamicWorkloadTest(View view) { + launchTestActivity(DynamicWorkloadActivity.class); + } + + public void onLaunchColdStartLatencyTest(View view) { + launchTestActivity(TestColdStartLatencyActivity.class); + } + + public void onLaunchRapidCycleTest(View view) { + launchTestActivity(TestRapidCycleActivity.class); + } +} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/FastButton.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/FastButton.java similarity index 91% rename from mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/FastButton.java rename to mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/FastButton.java index 7abda73e..5445f179 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/FastButton.java +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/FastButton.java @@ -14,18 +14,11 @@ * limitations under the License. */ -package com.google.sample.oboe.manualtest; +package com.mobileer.oboetester; import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.drawable.Drawable; -import android.text.TextPaint; import android.util.AttributeSet; import android.view.MotionEvent; -import android.view.View; import android.widget.TextView; import java.util.ArrayList; diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/GlitchActivity.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/GlitchActivity.java similarity index 51% rename from mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/GlitchActivity.java rename to mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/GlitchActivity.java index 4fb9f1fb..eddf8913 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/GlitchActivity.java +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/GlitchActivity.java @@ -14,16 +14,15 @@ * limitations under the License. */ -package com.google.sample.oboe.manualtest; +package com.mobileer.oboetester; import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; import android.view.View; import android.widget.Button; import android.widget.TextView; import java.io.IOException; +import java.util.Locale; /** * Activity to measure the number of glitches. @@ -42,14 +41,28 @@ public class GlitchActivity extends AnalyzerActivity { final static int STATE_LOCKED = 4; final static int STATE_GLITCHING = 5; String mLastGlitchReport; + private int mInputChannel; + private int mOutputChannel; native int getStateFrameCount(int state); native int getGlitchCount(); + + // Number of frames in last glitch. + native int getGlitchLength(); + native double getPhase(); native double getSignalToNoiseDB(); native double getPeakAmplitude(); + native double getSineAmplitude(); + native int getSinePeriod(); + + protected NativeSniffer mNativeSniffer = createNativeSniffer(); + + synchronized NativeSniffer createNativeSniffer() { + return new GlitchSniffer(); + } // Note that these strings must match the enum result_code in LatencyAnalyzer.h - String stateToString(int resultCode) { + static String stateToString(int resultCode) { switch (resultCode) { case STATE_IDLE: return "IDLE"; @@ -68,10 +81,12 @@ public class GlitchActivity extends AnalyzerActivity { } } + static String magnitudeToString(double magnitude) { + return String.format(Locale.US, "%6.4f", magnitude); + } + // Periodically query for glitches from the native detector. - protected class GlitchSniffer { - public static final int SNIFFER_UPDATE_PERIOD_MSEC = 100; - public static final int SNIFFER_UPDATE_DELAY_MSEC = 200; + protected class GlitchSniffer extends NativeSniffer { private long mTimeAtStart; private long mTimeOfLastGlitch; @@ -88,10 +103,10 @@ public class GlitchActivity extends AnalyzerActivity { private double mSignalToNoiseDB; private double mPeakAmplitude; - private Handler mHandler = new Handler(Looper.getMainLooper()); // UI thread - private volatile boolean mEnabled = true; + private double mSineAmplitude; - private void startSniffer() { + @Override + public void startSniffer() { long now = System.currentTimeMillis(); mTimeAtStart = now; mTimeOfLastGlitch = now; @@ -102,106 +117,101 @@ public class GlitchActivity extends AnalyzerActivity { mMaxSecondsWithoutGlitches = 0.0; mLastGlitchCount = 0; mStartResetCount = mLastResetCount; - // Start the initial runnable task by posting through the handler - mEnabled = true; - mHandler.postDelayed(runnableCode, SNIFFER_UPDATE_DELAY_MSEC); + super.startSniffer(); } - private void stopSniffer() { - mEnabled = false; - if (mHandler != null) { - mHandler.removeCallbacks(runnableCode); - } - - runOnUiThread(new Runnable() { - @Override - public void run() { - updateStatusText(); + private void gatherData() { + int state = getAnalyzerState(); + mSignalToNoiseDB = getSignalToNoiseDB(); + mPeakAmplitude = getPeakAmplitude(); + mSineAmplitude = getSineAmplitude(); + int glitchCount = getGlitchCount(); + if (state != mPreviousState) { + if ((state == STATE_WAITING_FOR_SIGNAL || state == STATE_WAITING_FOR_LOCK) + && glitchCount == 0) { // did not previously lock + GlitchActivity.this.giveAdvice("Try raising volume!"); + } else { + GlitchActivity.this.giveAdvice(null); } - }); - } + } + mPreviousState = state; - private Runnable runnableCode = new Runnable() { - @Override - public void run() { - int state = getAnalyzerState(); - mSignalToNoiseDB = getSignalToNoiseDB(); - mPeakAmplitude = getPeakAmplitude(); - mPreviousState = state; + long now = System.currentTimeMillis(); + int resetCount = getResetCount(); + mLastUnlockedFrames = getStateFrameCount(STATE_WAITING_FOR_LOCK); + int lockedFrames = getStateFrameCount(STATE_LOCKED); + int glitchFrames = getStateFrameCount(STATE_GLITCHING); - long now = System.currentTimeMillis(); - int glitchCount = getGlitchCount(); - int resetCount = getResetCount(); - mLastUnlockedFrames = getStateFrameCount(STATE_WAITING_FOR_LOCK); - int lockedFrames = getStateFrameCount(STATE_LOCKED); - int glitchFrames = getStateFrameCount(STATE_GLITCHING); - - if (glitchFrames > mLastGlitchFrames || glitchCount > mLastGlitchCount) { - mTimeOfLastGlitch = now; - mSecondsWithoutGlitches = 0.0; + if (glitchFrames > mLastGlitchFrames || glitchCount > mLastGlitchCount) { + mTimeOfLastGlitch = now; + mSecondsWithoutGlitches = 0.0; + if (glitchCount > mLastGlitchCount) { onGlitchDetected(); - } else if (lockedFrames > mLastLockedFrames) { - mSecondsWithoutGlitches = (now - mTimeOfLastGlitch) / 1000.0; - } - - if (resetCount > mLastResetCount) { - mLastResetCount = resetCount; - } - - if (mSecondsWithoutGlitches > mMaxSecondsWithoutGlitches) { - mMaxSecondsWithoutGlitches = mSecondsWithoutGlitches; - } - - mLastGlitchCount = glitchCount; - mLastGlitchFrames = glitchFrames; - mLastLockedFrames = lockedFrames; - mLastResetCount = resetCount; - - updateStatusText(); - - // Reschedule so this task repeats - if (mEnabled) { - mHandler.postDelayed(runnableCode, SNIFFER_UPDATE_PERIOD_MSEC); } + } else if (lockedFrames > mLastLockedFrames) { + mSecondsWithoutGlitches = (now - mTimeOfLastGlitch) / 1000.0; } - }; - String getCurrentStatusReport() { + if (resetCount > mLastResetCount) { + mLastResetCount = resetCount; + } + + if (mSecondsWithoutGlitches > mMaxSecondsWithoutGlitches) { + mMaxSecondsWithoutGlitches = mSecondsWithoutGlitches; + } + + mLastGlitchCount = glitchCount; + mLastGlitchFrames = glitchFrames; + mLastLockedFrames = lockedFrames; + mLastResetCount = resetCount; + } + + private String getCurrentStatusReport() { long now = System.currentTimeMillis(); double totalSeconds = (now - mTimeAtStart) / 1000.0; StringBuffer message = new StringBuffer(); message.append("state = " + stateToString(mPreviousState) + "\n"); - message.append(String.format("unlocked.frames = %d\n", mLastUnlockedFrames)); - message.append(String.format("locked.frames = %d\n", mLastLockedFrames)); - message.append(String.format("glitch.frames = %d\n", mLastGlitchFrames)); - message.append(String.format("reset.count = %d\n", mLastResetCount - mStartResetCount)); - message.append(String.format("peak.amplitude = %8.6f\n", mPeakAmplitude)); + message.append(String.format(Locale.getDefault(), "unlocked.frames = %d\n", mLastUnlockedFrames)); + message.append(String.format(Locale.getDefault(), "locked.frames = %d\n", mLastLockedFrames)); + message.append(String.format(Locale.getDefault(), "glitch.frames = %d\n", mLastGlitchFrames)); + message.append(String.format(Locale.getDefault(), "reset.count = %d\n", mLastResetCount - mStartResetCount)); + message.append(String.format(Locale.getDefault(), "peak.amplitude = %8.6f\n", mPeakAmplitude)); + message.append(String.format(Locale.getDefault(), "sine.amplitude = %8.6f\n", mSineAmplitude)); if (mLastLockedFrames > 0) { - message.append(String.format("signal.noise.ratio.db = %5.1f\n", mSignalToNoiseDB)); + message.append(String.format(Locale.getDefault(), "signal.noise.ratio.db = %5.1f\n", mSignalToNoiseDB)); } - message.append(String.format("time.total = %8.2f seconds\n", totalSeconds)); + message.append(String.format(Locale.getDefault(), "time.total = %4.2f seconds\n", totalSeconds)); if (mLastLockedFrames > 0) { - message.append(String.format("time.no.glitches = %8.2f\n", mSecondsWithoutGlitches)); - message.append(String.format("max.time.no.glitches = %8.2f\n", + message.append(String.format(Locale.getDefault(), "time.no.glitches = %4.2f\n", mSecondsWithoutGlitches)); + message.append(String.format(Locale.getDefault(), "max.time.no.glitches = %4.2f\n", mMaxSecondsWithoutGlitches)); - message.append(String.format("glitch.count = %d\n", mLastGlitchCount)); + message.append(String.format(Locale.getDefault(), "glitch.length = %d\n", getGlitchLength())); + message.append(String.format(Locale.getDefault(), "glitch.count = %d\n", mLastGlitchCount)); } return message.toString(); } public String getShortReport() { - String resultText = "#glitches = " + getLastGlitchCount() + String resultText = "amplitude: peak = " + magnitudeToString(mPeakAmplitude) + + ", sine = " + magnitudeToString(mSineAmplitude) + "\n"; + if (mPeakAmplitude < 0.01) { + resultText += "WARNING: volume is very low!\n"; + } + resultText += "#glitches = " + getLastGlitchCount() + ", #resets = " + getLastResetCount() + ", max no glitch = " + getMaxSecondsWithNoGlitch() + " secs\n"; - resultText += String.format("SNR = %5.1f db", mSignalToNoiseDB); + resultText += String.format(Locale.getDefault(), "SNR = %5.1f db", mSignalToNoiseDB); resultText += ", #locked = " + mLastLockedFrames; return resultText; } - private void updateStatusText() { + @Override + public void updateStatusText() { + gatherData(); mLastGlitchReport = getCurrentStatusReport(); setAnalyzerText(mLastGlitchReport); + maybeDisplayWaveform(); } public double getMaxSecondsWithNoGlitch() { @@ -216,23 +226,55 @@ public class GlitchActivity extends AnalyzerActivity { } } + public void giveAdvice(String s) { + } + // Called on UI thread protected void onGlitchDetected() { } - private GlitchSniffer mGlitchSniffer = new GlitchSniffer(); + protected void maybeDisplayWaveform() {} - private void setAnalyzerText(String s) { + protected void setAnalyzerText(String s) { mAnalyzerTextView.setText(s); } /** * Set tolerance to deviations from expected value. - * The normalized value will be converted in the native code. + * The normalized value will be scaled by the measured magnitude + * of the sine wave.. * @param tolerance normalized between 0.0 and 1.0 */ public native void setTolerance(float tolerance); + public void setInputChannel(int channel) { + mInputChannel = channel; + setInputChannelNative(channel); + } + + public void setOutputChannel(int channel) { + mOutputChannel = channel; + setOutputChannelNative(channel); + } + + public int getInputChannel() { + return mInputChannel; + } + + public int getOutputChannel() { + return mOutputChannel; + } + + /** + * Set the duration of a periodic forced glitch. + * @param frames or zero for no glitch + */ + public native void setForcedGlitchDuration(int frames); + + public native void setInputChannelNative(int channel); + + public native void setOutputChannelNative(int channel); + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -253,23 +295,45 @@ public class GlitchActivity extends AnalyzerActivity { } } + @Override + int getActivityType() { + return ACTIVITY_GLITCHES; + } + @Override protected void onStart() { super.onStart(); - setActivityType(ACTIVITY_GLITCHES); - mStartButton.setEnabled(true); - mStopButton.setEnabled(false); - mShareButton.setEnabled(false); + setInputChannel(0); + setOutputChannel(0); } @Override protected void onStop() { - stopAudioTest(); + if (!isBackgroundEnabled()) { + stopAudioTest(); + } super.onStop(); } + @Override + protected void onDestroy() { + if (isBackgroundEnabled()) { + stopAudioTest(); + } + super.onDestroy(); + } + // Called on UI thread - public void onStartAudioTest(View view) throws IOException { + public void onStartAudioTest(View view) { + try { + openStartAudioTestUI(); + } catch (IOException e) { + showErrorToast(e.getMessage()); + } + } + + protected void openStartAudioTestUI() throws IOException { + openAudio(); startAudioTest(); mStartButton.setEnabled(false); mStopButton.setEnabled(true); @@ -278,17 +342,11 @@ public class GlitchActivity extends AnalyzerActivity { } public void startAudioTest() throws IOException { - openAudio(); startAudio(); - mGlitchSniffer.startSniffer(); + mNativeSniffer.startSniffer(); onTestBegan(); } - public void onCancel(View view) { - stopAudioTest(); - onTestFinished(); - } - // Called on UI thread public void onStopAudioTest(View view) { stopAudioTest(); @@ -308,26 +366,27 @@ public class GlitchActivity extends AnalyzerActivity { } public void stopAudioTest() { - mGlitchSniffer.stopSniffer(); + mNativeSniffer.stopSniffer(); stopAudio(); closeAudio(); } + public void stopTest() { + mNativeSniffer.stopSniffer(); + stopAudio(); + } + @Override boolean isOutput() { return false; } - @Override - public void setupEffects(int sessionId) { - } - public double getMaxSecondsWithNoGlitch() { - return mGlitchSniffer.getMaxSecondsWithNoGlitch(); + return ((GlitchSniffer)mNativeSniffer).getMaxSecondsWithNoGlitch(); } public String getShortReport() { - return mGlitchSniffer.getShortReport(); + return ((GlitchSniffer)mNativeSniffer).getShortReport(); } @Override diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/InputMarginView.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/InputMarginView.java similarity index 93% rename from mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/InputMarginView.java rename to mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/InputMarginView.java index 11b32522..32a8028b 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/InputMarginView.java +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/InputMarginView.java @@ -14,17 +14,13 @@ * limitations under the License. */ -package com.google.sample.oboe.manualtest; +package com.mobileer.oboetester; import android.content.Context; import android.util.AttributeSet; import android.view.LayoutInflater; -import android.view.View; -import android.widget.CheckBox; import android.widget.LinearLayout; -import java.util.ArrayList; - /** * View for editing an input stream margin to avoid glitches. * diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/IntentBasedTestSupport.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/IntentBasedTestSupport.java new file mode 100644 index 00000000..5f2d00e7 --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/IntentBasedTestSupport.java @@ -0,0 +1,377 @@ +/* + * 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. + */ + +package com.mobileer.oboetester; + +import android.media.AudioManager; +import android.os.Bundle; + +public class IntentBasedTestSupport { + + public static final String KEY_IN_SHARING = "in_sharing"; + public static final String KEY_OUT_SHARING = "out_sharing"; + public static final String VALUE_SHARING_EXCLUSIVE = "exclusive"; + public static final String VALUE_SHARING_SHARED = "shared"; + + public static final String KEY_IN_PERF = "in_perf"; + public static final String KEY_OUT_PERF = "out_perf"; + public static final String VALUE_PERF_LOW_LATENCY = "lowlat"; + public static final String VALUE_PERF_POWERSAVE = "powersave"; + public static final String VALUE_PERF_NONE = "none"; + + public static final String KEY_IN_CHANNELS = "in_channels"; + public static final String KEY_OUT_CHANNELS = "out_channels"; + public static final int VALUE_DEFAULT_CHANNELS = 2; + + public static final String KEY_IN_USE_MMAP = "in_use_mmap"; + public static final String KEY_OUT_USE_MMAP = "out_use_mmap"; + public static final boolean VALUE_DEFAULT_USE_MMAP = NativeEngine.isMMapSupported(); + + public static final String KEY_IN_PRESET = "in_preset"; + public static final String KEY_SAMPLE_RATE = "sample_rate"; + public static final int VALUE_DEFAULT_SAMPLE_RATE = 48000; + public static final String VALUE_UNSPECIFIED = "unspecified"; + + public static final String KEY_OUT_USAGE = "out_usage"; + public static final String VALUE_USAGE_MEDIA = "media"; + public static final String VALUE_USAGE_VOICE_COMMUNICATION = "voice_communication"; + public static final String VALUE_USAGE_ALARM = "alarm"; + public static final String VALUE_USAGE_NOTIFICATION = "notification"; + public static final String VALUE_USAGE_GAME = "game"; + + public static final String KEY_IN_API = "in_api"; + public static final String KEY_OUT_API = "out_api"; + public static final String VALUE_API_AAUDIO = "aaudio"; + public static final String VALUE_API_OPENSLES = "opensles"; + + public static final String KEY_FILE_NAME = "file"; + public static final String KEY_BUFFER_BURSTS = "buffer_bursts"; + public static final String KEY_BACKGROUND = "background"; + public static final String KEY_FOREGROUND_SERVICE = "foreground_service"; + public static final String KEY_VOLUME = "volume"; + + public static final String KEY_VOLUME_TYPE = "volume_type"; + public static final float VALUE_VOLUME_INVALID = -1.0f; + public static final String VALUE_VOLUME_TYPE_ACCESSIBILITY = "accessibility"; + public static final String VALUE_VOLUME_TYPE_ALARM = "alarm"; + public static final String VALUE_VOLUME_TYPE_DTMF = "dtmf"; + public static final String VALUE_VOLUME_TYPE_MUSIC = "music"; + public static final String VALUE_VOLUME_TYPE_NOTIFICATION = "notification"; + public static final String VALUE_VOLUME_TYPE_RING = "ring"; + public static final String VALUE_VOLUME_TYPE_SYSTEM = "system"; + public static final String VALUE_VOLUME_TYPE_VOICE_CALL = "voice_call"; + + public static final String KEY_IN_CHANNEL_MASK = "in_channel_mask"; + public static final String KEY_OUT_CHANNEL_MASK = "out_channel_mask"; + public static final String VALUE_CHANNEL_MONO = "mono"; + public static final String VALUE_CHANNEL_STEREO = "stereo"; + public static final String VALUE_CHANNEL_2POINT1 = "2.1"; + public static final String VALUE_CHANNEL_TRI = "tri"; + public static final String VALUE_CHANNEL_TRI_BACK = "triBack"; + public static final String VALUE_CHANNEL_TRI_BACK_LOWERCASE = "triback"; + public static final String VALUE_CHANNEL_3POINT1 = "3.1"; + public static final String VALUE_CHANNEL_2POINT0POINT2 = "2.0.2"; + public static final String VALUE_CHANNEL_2POINT1POINT2 = "2.1.2"; + public static final String VALUE_CHANNEL_3POINT0POINT2 = "3.0.2"; + public static final String VALUE_CHANNEL_3POINT1POINT2 = "3.1.2"; + public static final String VALUE_CHANNEL_QUAD = "quad"; + public static final String VALUE_CHANNEL_QUAD_SIDE = "quadSide"; + public static final String VALUE_CHANNEL_QUAD_SIDE_LOWERCASE = "quadside"; + public static final String VALUE_CHANNEL_SURROUND = "surround"; + public static final String VALUE_CHANNEL_PENTA = "penta"; + public static final String VALUE_CHANNEL_5POINT1 = "5.1"; + public static final String VALUE_CHANNEL_5POINT1_SIDE = "5.1Side"; + public static final String VALUE_CHANNEL_5POINT1_SIDE_LOWERCASE = "5.1side"; + public static final String VALUE_CHANNEL_6POINT1 = "6.1"; + public static final String VALUE_CHANNEL_7POINT1 = "7.1"; + public static final String VALUE_CHANNEL_5POINT1POINT2 = "5.1.2"; + public static final String VALUE_CHANNEL_5POINT1POINT4 = "5.1.4"; + public static final String VALUE_CHANNEL_7POINT1POINT2 = "7.1.2"; + public static final String VALUE_CHANNEL_7POINT1POINT4 = "7.1.4"; + public static final String VALUE_CHANNEL_9POINT1POINT4 = "9.1.4"; + public static final String VALUE_CHANNEL_9POINT1POINT6 = "9.1.6"; + public static final String VALUE_CHANNEL_FRONT_BACK = "frontBack"; + public static final String VALUE_CHANNEL_FRONT_BACK_LOWERCASE = "frontback"; + + public static final String KEY_SIGNAL_TYPE = "signal_type"; + public static final String VALUE_SIGNAL_SINE = "sine"; + public static final String VALUE_SIGNAL_SAWTOOTH = "sawtooth"; + public static final String VALUE_SIGNAL_FREQ_SWEEP = "freq_sweep"; + public static final String VALUE_SIGNAL_PITCH_SWEEP = "pitch_sweep"; + public static final String VALUE_SIGNAL_WHITE_NOISE = "white_noise"; + + public static final String KEY_DURATION = "duration"; + public static final int VALUE_DEFAULT_DURATION = 10; + + public static int getApiFromText(String text) { + if (VALUE_API_AAUDIO.equals(text)) { + return StreamConfiguration.NATIVE_API_AAUDIO; + } else if (VALUE_API_OPENSLES.equals(text)) { + return StreamConfiguration.NATIVE_API_OPENSLES; + } else { + return StreamConfiguration.NATIVE_API_UNSPECIFIED; + } + } + + public static int getPerfFromText(String text) { + if (VALUE_PERF_NONE.equals(text)) { + return StreamConfiguration.PERFORMANCE_MODE_NONE; + } else if (VALUE_PERF_POWERSAVE.equals(text)) { + return StreamConfiguration.PERFORMANCE_MODE_POWER_SAVING; + } else if (VALUE_PERF_LOW_LATENCY.equals(text)) { + return StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY; + } else { + throw new IllegalArgumentException("perf mode invalid: " + text); + } + } + + public static int getSharingFromText(String text) { + if (VALUE_SHARING_SHARED.equals(text)) { + return StreamConfiguration.SHARING_MODE_SHARED; + } else { + return StreamConfiguration.SHARING_MODE_EXCLUSIVE; + } + } + public static int getUsageFromText(String text) { + if (VALUE_USAGE_GAME.equals(text)) { + return StreamConfiguration.USAGE_GAME; + } else if (VALUE_USAGE_VOICE_COMMUNICATION.equals(text)) { + return StreamConfiguration.USAGE_VOICE_COMMUNICATION; + } else if (VALUE_USAGE_MEDIA.equals(text)) { + return StreamConfiguration.USAGE_MEDIA; + } else if (VALUE_USAGE_ALARM.equals(text)) { + return StreamConfiguration.USAGE_ALARM; + } else if (VALUE_USAGE_NOTIFICATION.equals(text)) { + return StreamConfiguration.USAGE_NOTIFICATION; + } else { + return StreamConfiguration.UNSPECIFIED; + } + } + + public static void configureStreamsFromBundle(Bundle bundle, + StreamConfiguration requestedInConfig, + StreamConfiguration requestedOutConfig) { + configureInputStreamFromBundle(bundle, requestedInConfig); + configureOutputStreamFromBundle(bundle, requestedOutConfig); + } + + public static float getNormalizedVolumeFromBundle(Bundle bundle) { + return bundle.getFloat(KEY_VOLUME, VALUE_VOLUME_INVALID); + } + + /** + * @param bundle + * @return AudioManager.STREAM type or throw IllegalArgumentException + */ + public static int getVolumeStreamTypeFromBundle(Bundle bundle) { + String typeText = bundle.getString(KEY_VOLUME_TYPE, VALUE_VOLUME_TYPE_MUSIC); + switch (typeText) { + case VALUE_VOLUME_TYPE_ACCESSIBILITY: + return AudioManager.STREAM_ACCESSIBILITY; + case VALUE_VOLUME_TYPE_ALARM: + return AudioManager.STREAM_ALARM; + case VALUE_VOLUME_TYPE_DTMF: + return AudioManager.STREAM_DTMF; + case VALUE_VOLUME_TYPE_MUSIC: + return AudioManager.STREAM_MUSIC; + case VALUE_VOLUME_TYPE_NOTIFICATION: + return AudioManager.STREAM_NOTIFICATION; + case VALUE_VOLUME_TYPE_RING: + return AudioManager.STREAM_RING; + case VALUE_VOLUME_TYPE_SYSTEM: + return AudioManager.STREAM_SYSTEM; + case VALUE_VOLUME_TYPE_VOICE_CALL: + return AudioManager.STREAM_VOICE_CALL; + default: + throw new IllegalArgumentException(KEY_VOLUME_TYPE + " invalid: " + typeText); + } + } + + public static int getChannelMaskFromBundle(Bundle bundle, String channelMaskKey) { + String channelMaskText = bundle.getString(channelMaskKey); + if (channelMaskText == null) { + return StreamConfiguration.UNSPECIFIED; + } + switch (channelMaskText) { + case VALUE_CHANNEL_MONO: + return StreamConfiguration.CHANNEL_MONO; + case VALUE_CHANNEL_STEREO: + return StreamConfiguration.CHANNEL_STEREO; + case VALUE_CHANNEL_2POINT1: + return StreamConfiguration.CHANNEL_2POINT1; + case VALUE_CHANNEL_TRI: + return StreamConfiguration.CHANNEL_TRI; + case VALUE_CHANNEL_TRI_BACK: + case VALUE_CHANNEL_TRI_BACK_LOWERCASE: + return StreamConfiguration.CHANNEL_TRI_BACK; + case VALUE_CHANNEL_3POINT1: + return StreamConfiguration.CHANNEL_3POINT1; + case VALUE_CHANNEL_2POINT0POINT2: + return StreamConfiguration.CHANNEL_2POINT0POINT2; + case VALUE_CHANNEL_2POINT1POINT2: + return StreamConfiguration.CHANNEL_2POINT1POINT2; + case VALUE_CHANNEL_3POINT0POINT2: + return StreamConfiguration.CHANNEL_3POINT0POINT2; + case VALUE_CHANNEL_3POINT1POINT2: + return StreamConfiguration.CHANNEL_3POINT1POINT2; + case VALUE_CHANNEL_QUAD: + return StreamConfiguration.CHANNEL_QUAD; + case VALUE_CHANNEL_QUAD_SIDE: + case VALUE_CHANNEL_QUAD_SIDE_LOWERCASE: + return StreamConfiguration.CHANNEL_QUAD_SIDE; + case VALUE_CHANNEL_SURROUND: + return StreamConfiguration.CHANNEL_SURROUND; + case VALUE_CHANNEL_PENTA: + return StreamConfiguration.CHANNEL_PENTA; + case VALUE_CHANNEL_5POINT1: + return StreamConfiguration.CHANNEL_5POINT1; + case VALUE_CHANNEL_5POINT1_SIDE: + case VALUE_CHANNEL_5POINT1_SIDE_LOWERCASE: + return StreamConfiguration.CHANNEL_5POINT1_SIDE; + case VALUE_CHANNEL_6POINT1: + return StreamConfiguration.CHANNEL_6POINT1; + case VALUE_CHANNEL_7POINT1: + return StreamConfiguration.CHANNEL_7POINT1; + case VALUE_CHANNEL_5POINT1POINT2: + return StreamConfiguration.CHANNEL_5POINT1POINT2; + case VALUE_CHANNEL_5POINT1POINT4: + return StreamConfiguration.CHANNEL_5POINT1POINT4; + case VALUE_CHANNEL_7POINT1POINT2: + return StreamConfiguration.CHANNEL_7POINT1POINT2; + case VALUE_CHANNEL_7POINT1POINT4: + return StreamConfiguration.CHANNEL_7POINT1POINT4; + case VALUE_CHANNEL_9POINT1POINT4: + return StreamConfiguration.CHANNEL_9POINT1POINT4; + case VALUE_CHANNEL_9POINT1POINT6: + return StreamConfiguration.CHANNEL_9POINT1POINT6; + case VALUE_CHANNEL_FRONT_BACK: + case VALUE_CHANNEL_FRONT_BACK_LOWERCASE: + return StreamConfiguration.CHANNEL_FRONT_BACK; + default: + throw new IllegalArgumentException( + channelMaskKey + " invalid: " + channelMaskText); + } + } + + public static void configureOutputStreamFromBundle(Bundle bundle, + StreamConfiguration requestedOutConfig) { + int audioApi; + String text; + + requestedOutConfig.reset(); + + int sampleRate = bundle.getInt(KEY_SAMPLE_RATE, VALUE_DEFAULT_SAMPLE_RATE); + requestedOutConfig.setSampleRate(sampleRate); + + text = bundle.getString(KEY_OUT_API, VALUE_UNSPECIFIED); + audioApi = getApiFromText(text); + requestedOutConfig.setNativeApi(audioApi); + + int outChannels = bundle.getInt(KEY_OUT_CHANNELS, VALUE_DEFAULT_CHANNELS); + int channelMask = getChannelMaskFromBundle(bundle, KEY_OUT_CHANNEL_MASK); + // Respect channel mask when it is specified. + if (channelMask != StreamConfiguration.UNSPECIFIED) { + requestedOutConfig.setChannelMask(channelMask); + } else { + requestedOutConfig.setChannelCount(outChannels); + } + + boolean outMMAP = bundle.getBoolean(KEY_OUT_USE_MMAP, VALUE_DEFAULT_USE_MMAP); + requestedOutConfig.setMMap(outMMAP); + + text = bundle.getString(KEY_OUT_PERF, VALUE_PERF_LOW_LATENCY); + int perfMode = getPerfFromText(text); + requestedOutConfig.setPerformanceMode(perfMode); + + text = bundle.getString(KEY_OUT_SHARING, VALUE_SHARING_EXCLUSIVE); + int sharingMode = getSharingFromText(text); + requestedOutConfig.setSharingMode(sharingMode); + + text = bundle.getString(KEY_OUT_USAGE, VALUE_USAGE_MEDIA); + int usage = getUsageFromText(text); + requestedOutConfig.setUsage(usage); + + + } + + public static void configureInputStreamFromBundle(Bundle bundle, + StreamConfiguration requestedInConfig) { + int audioApi; + String text; + + requestedInConfig.reset(); + + int sampleRate = bundle.getInt(KEY_SAMPLE_RATE, VALUE_DEFAULT_SAMPLE_RATE); + requestedInConfig.setSampleRate(sampleRate); + + text = bundle.getString(KEY_IN_API, VALUE_UNSPECIFIED); + audioApi = getApiFromText(text); + requestedInConfig.setNativeApi(audioApi); + + int inChannels = bundle.getInt(KEY_IN_CHANNELS, VALUE_DEFAULT_CHANNELS); + int channelMask = getChannelMaskFromBundle(bundle, KEY_IN_CHANNEL_MASK); + // Respect channel mask when it is specified. + if (channelMask != StreamConfiguration.UNSPECIFIED) { + requestedInConfig.setChannelMask(channelMask); + } else { + requestedInConfig.setChannelCount(inChannels); + } + + boolean inMMAP = bundle.getBoolean(KEY_IN_USE_MMAP, VALUE_DEFAULT_USE_MMAP); + requestedInConfig.setMMap(inMMAP); + + text = bundle.getString(KEY_IN_PERF, VALUE_PERF_LOW_LATENCY); + int perfMode = getPerfFromText(text); + requestedInConfig.setPerformanceMode(perfMode); + + text = bundle.getString(KEY_IN_SHARING, VALUE_SHARING_EXCLUSIVE); + int sharingMode = getSharingFromText(text); + requestedInConfig.setSharingMode(sharingMode); + + String defaultText = StreamConfiguration.convertInputPresetToText( + StreamConfiguration.INPUT_PRESET_VOICE_RECOGNITION); + text = bundle.getString(KEY_IN_PRESET, defaultText); + int inputPreset = StreamConfiguration.convertTextToInputPreset(text); + if (inputPreset < 0) throw new IllegalArgumentException(KEY_IN_PRESET + " invalid: " + text); + requestedInConfig.setInputPreset(inputPreset); + } + + public static int getSignalTypeFromBundle(Bundle bundle) { + String signalTypeText = bundle.getString(KEY_SIGNAL_TYPE); + if (signalTypeText == null) { + return 0; + } + switch (signalTypeText) { + case VALUE_SIGNAL_SINE: + return 0; + case VALUE_SIGNAL_SAWTOOTH: + return 1; + case VALUE_SIGNAL_FREQ_SWEEP: + return 2; + case VALUE_SIGNAL_PITCH_SWEEP: + return 3; + case VALUE_SIGNAL_WHITE_NOISE: + return 4; + default: + throw new IllegalArgumentException( + KEY_SIGNAL_TYPE + " invalid: " + signalTypeText); + } + } + + public static int getDurationSeconds(Bundle bundle) { + return bundle.getInt(KEY_DURATION, VALUE_DEFAULT_DURATION); + } +} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/MainActivity.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/MainActivity.java similarity index 54% rename from mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/MainActivity.java rename to mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/MainActivity.java index 6a7371c5..5e3d19f8 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/MainActivity.java +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/MainActivity.java @@ -14,9 +14,10 @@ * limitations under the License. */ -package com.google.sample.oboe.manualtest; +package com.mobileer.oboetester; + +import static com.mobileer.oboetester.AudioQueryTools.getSystemProperty; -import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.pm.PackageInfo; @@ -32,30 +33,36 @@ import android.widget.AdapterView; import android.widget.CheckBox; import android.widget.Spinner; import android.widget.TextView; -import android.widget.Toast; /** * Select various Audio tests. */ -public class MainActivity extends Activity { +public class MainActivity extends BaseOboeTesterActivity { private static final String KEY_TEST_NAME = "test"; public static final String VALUE_TEST_NAME_LATENCY = "latency"; public static final String VALUE_TEST_NAME_GLITCH = "glitch"; + public static final String VALUE_TEST_NAME_DATA_PATHS = "data_paths"; + public static final String VALUE_TEST_NAME_OUTPUT = "output"; + public static final String VALUE_TEST_NAME_INPUT = "input"; + public static final String VALUE_TEST_NAME_CPU_LOAD = "cpu_load"; static { // Must match name in CMakeLists.txt System.loadLibrary("oboetester"); } - private Spinner mModeSpinner; - private TextView mCallbackSizeTextView; + private TextView mCallbackSizeEditor; protected TextView mDeviceView; private TextView mVersionTextView; private TextView mBuildTextView; private Bundle mBundleFromIntent; + private CheckBox mWorkaroundsCheckBox; + private CheckBox mBackgroundCheckBox; + private CheckBox mForegroundServiceCheckBox; + private static String mVersionText; @Override protected void onCreate(Bundle savedInstanceState) { @@ -65,13 +72,14 @@ public class MainActivity extends Activity { logScreenSize(); mVersionTextView = (TextView) findViewById(R.id.versionText); - mCallbackSizeTextView = (TextView) findViewById(R.id.callbackSize); + mCallbackSizeEditor = (TextView) findViewById(R.id.callbackSize); mDeviceView = (TextView) findViewById(R.id.deviceView); updateNativeAudioUI(); // Set mode, eg. MODE_IN_COMMUNICATION mModeSpinner = (Spinner) findViewById(R.id.spinnerAudioMode); + // Update AudioManager now in case user is trying to affect a different app. mModeSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView adapterView, View view, int i, long l) { @@ -91,19 +99,33 @@ public class MainActivity extends Activity { int oboeMajor = (oboeVersion >> 24) & 0xFF; int oboeMinor = (oboeVersion >> 16) & 0xFF; int oboePatch = oboeVersion & 0xFF; - mVersionTextView.setText("Test v (" + pinfo.versionCode + ") " + pinfo.versionName - + ", Oboe v " + oboeMajor + "." + oboeMinor + "." + oboePatch); + mVersionText = getString(R.string.app_name_version, + pinfo.versionCode, pinfo.versionName, + oboeMajor, oboeMinor, oboePatch); + mVersionTextView.setText(mVersionText); } catch (PackageManager.NameNotFoundException e) { mVersionTextView.setText(e.getMessage()); } - mBuildTextView = (TextView) findViewById(R.id.text_build_info); - mBuildTextView.setText(Build.DISPLAY); + mWorkaroundsCheckBox = (CheckBox) findViewById(R.id.boxEnableWorkarounds); + // Turn off workarounds so we can test the underlying API bugs. + mWorkaroundsCheckBox.setChecked(false); + NativeEngine.setWorkaroundsEnabled(false); + mBackgroundCheckBox = (CheckBox) findViewById(R.id.boxEnableBackground); + mForegroundServiceCheckBox = (CheckBox) findViewById(R.id.boxEnableForegroundService); + + mBuildTextView = (TextView) findViewById(R.id.text_build_info); + mBuildTextView.setText(Build.DISPLAY + + "\n" + getSystemProperty("ro.build.date")); saveIntentBundleForLaterProcessing(getIntent()); } + public static String getVersionText() { + return mVersionText; + } + private void logScreenSize() { Display display = getWindowManager().getDefaultDisplay(); Point size = new Point(); @@ -115,6 +137,7 @@ public class MainActivity extends Activity { @Override public void onNewIntent(Intent intent) { + super.onNewIntent(intent); saveIntentBundleForLaterProcessing(intent); } @@ -127,26 +150,54 @@ public class MainActivity extends Activity { if (mBundleFromIntent == null) { return; } - - if (mBundleFromIntent.containsKey(KEY_TEST_NAME)) { - String testName = mBundleFromIntent.getString(KEY_TEST_NAME); - if (VALUE_TEST_NAME_LATENCY.equals(testName)) { - Intent intent = new Intent(this, RoundTripLatencyActivity.class); - intent.putExtras(mBundleFromIntent); - startActivity(intent); - } else if (VALUE_TEST_NAME_GLITCH.equals(testName)) { - Intent intent = new Intent(this, ManualGlitchActivity.class); - intent.putExtras(mBundleFromIntent); - startActivity(intent); - } + Intent intent = getTestIntent(mBundleFromIntent); + if (intent != null) { + setTogglesFromIntent(); + startActivity(intent); } mBundleFromIntent = null; } + private void setTogglesFromIntent() { + boolean backgroundEnabled = mBundleFromIntent.getBoolean( + IntentBasedTestSupport.KEY_BACKGROUND, false); + TestAudioActivity.setBackgroundEnabled(backgroundEnabled); + boolean foregroundServiceEnabled = mBundleFromIntent.getBoolean( + IntentBasedTestSupport.KEY_FOREGROUND_SERVICE, false); + TestAudioActivity.setBackgroundEnabled(foregroundServiceEnabled); + } + + private Intent getTestIntent(Bundle bundle) { + Intent intent = null; + if (bundle.containsKey(KEY_TEST_NAME)) { + String testName = bundle.getString(KEY_TEST_NAME); + if (VALUE_TEST_NAME_LATENCY.equals(testName)) { + intent = new Intent(this, RoundTripLatencyActivity.class); + intent.putExtras(bundle); + } else if (VALUE_TEST_NAME_GLITCH.equals(testName)) { + intent = new Intent(this, ManualGlitchActivity.class); + intent.putExtras(bundle); + } else if (VALUE_TEST_NAME_DATA_PATHS.equals(testName)) { + intent = new Intent(this, TestDataPathsActivity.class); + intent.putExtras(bundle); + } else if (VALUE_TEST_NAME_INPUT.equals(testName)) { + intent = new Intent(this, TestInputActivity.class); + intent.putExtras(bundle); + } else if (VALUE_TEST_NAME_OUTPUT.equals(testName)) { + intent = new Intent(this, TestOutputActivity.class); + intent.putExtras(bundle); + } else if (VALUE_TEST_NAME_CPU_LOAD.equals(testName)) { + intent = new Intent(this, DynamicWorkloadActivity.class); + intent.putExtras(bundle); + } + } + return intent; + } + @Override public void onResume(){ super.onResume(); - NativeEngine.setWorkaroundsEnabled(false); + mWorkaroundsCheckBox.setChecked(NativeEngine.areWorkaroundsEnabled()); processBundleFromIntent(); } @@ -159,57 +210,67 @@ public class MainActivity extends Activity { } public void onLaunchTestOutput(View view) { - updateCallbackSize(); - Intent intent = new Intent(this, TestOutputActivity.class); - startActivity(intent); + launchTestActivity(TestOutputActivity.class); } public void onLaunchTestInput(View view) { - updateCallbackSize(); - Intent intent = new Intent(this, TestInputActivity.class); - startActivity(intent); + launchTestThatDoesRecording(TestInputActivity.class); } public void onLaunchTapToTone(View view) { - updateCallbackSize(); - Intent intent = new Intent(this, TapToToneActivity.class); - startActivity(intent); + launchTestThatDoesRecording(TapToToneActivity.class); } public void onLaunchRecorder(View view) { - updateCallbackSize(); - Intent intent = new Intent(this, RecorderActivity.class); - startActivity(intent); + launchTestThatDoesRecording(RecorderActivity.class); } public void onLaunchEcho(View view) { - updateCallbackSize(); - Intent intent = new Intent(this, EchoActivity.class); - startActivity(intent); + launchTestThatDoesRecording(EchoActivity.class); } public void onLaunchRoundTripLatency(View view) { - updateCallbackSize(); - Intent intent = new Intent(this, RoundTripLatencyActivity.class); - startActivity(intent); + launchTestThatDoesRecording(RoundTripLatencyActivity.class); } public void onLaunchManualGlitchTest(View view) { - updateCallbackSize(); - Intent intent = new Intent(this, ManualGlitchActivity.class); - startActivity(intent); + launchTestThatDoesRecording(ManualGlitchActivity.class); } - public void onLaunchAutoGlitchTest(View view) { - updateCallbackSize(); - Intent intent = new Intent(this, AutoGlitchActivity.class); - startActivity(intent); - } + public void onLaunchAutoGlitchTest(View view) { launchTestThatDoesRecording(AutomatedGlitchActivity.class); } public void onLaunchTestDisconnect(View view) { + launchTestThatDoesRecording(TestDisconnectActivity.class); + } + + public void onLaunchTestDataPaths(View view) { + launchTestThatDoesRecording(TestDataPathsActivity.class); + } + + public void onLaunchTestDeviceReport(View view) { + launchTestActivity(DeviceReportActivity.class); + } + + public void onLaunchExtratests(View view) { + launchTestActivity(ExtraTestsActivity.class); + } + + private void applyUserOptions() { updateCallbackSize(); - Intent intent = new Intent(this, TestDisconnectActivity.class); - startActivity(intent); + + long mode = mModeSpinner.getSelectedItemId(); + AudioManager myAudioMgr = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + myAudioMgr.setMode((int) mode); + + NativeEngine.setWorkaroundsEnabled(mWorkaroundsCheckBox.isChecked()); + TestAudioActivity.setBackgroundEnabled(mBackgroundCheckBox.isChecked()); + TestAudioActivity.setForegroundServiceEnabled(mForegroundServiceCheckBox.isChecked()); + } + + @Override + protected void launchTestActivity(Class clazz) { + applyUserOptions(); + super.launchTestActivity(clazz); } public void onUseCallbackClicked(View view) { @@ -217,44 +278,17 @@ public class MainActivity extends Activity { OboeAudioStream.setUseCallback(checkBox.isChecked()); } - protected void showErrorToast(String message) { - showToast("Error: " + message); - } - - protected void showToast(final String message) { - runOnUiThread(new Runnable() { - @Override - public void run() { - Toast.makeText(MainActivity.this, - message, - Toast.LENGTH_SHORT).show(); - } - }); - } - private void updateCallbackSize() { - CharSequence chars = mCallbackSizeTextView.getText(); + CharSequence chars = mCallbackSizeEditor.getText(); String text = chars.toString(); int callbackSize = 0; try { callbackSize = Integer.parseInt(text); } catch (NumberFormatException e) { showErrorToast("Badly formated callback size: " + text); - mCallbackSizeTextView.setText("0"); + mCallbackSizeEditor.setText("0"); } OboeAudioStream.setCallbackSize(callbackSize); } - public void onSetSpeakerphoneOn(View view) { - CheckBox checkBox = (CheckBox) view; - boolean enabled = checkBox.isChecked(); - AudioManager myAudioMgr = (AudioManager) getSystemService(Context.AUDIO_SERVICE); - myAudioMgr.setSpeakerphoneOn(enabled); - } - - public void onEnableWorkarounds(View view) { - CheckBox checkBox = (CheckBox) view; - boolean enabled = checkBox.isChecked(); - NativeEngine.setWorkaroundsEnabled(enabled); - } } diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ManualGlitchActivity.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ManualGlitchActivity.java new file mode 100644 index 00000000..9281cd59 --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ManualGlitchActivity.java @@ -0,0 +1,291 @@ +/* + * 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. + */ + +package com.mobileer.oboetester; + +import android.content.Context; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import android.view.View; +import android.widget.CheckBox; +import android.widget.LinearLayout; +import android.widget.RadioButton; +import android.widget.RadioGroup; +import android.widget.SeekBar; +import android.widget.TextView; + +import java.io.IOException; +import java.util.Locale; + +public class ManualGlitchActivity extends GlitchActivity { + + public static final String KEY_BUFFER_BURSTS = "buffer_bursts"; + public static final int VALUE_DEFAULT_BUFFER_BURSTS = 2; + + public static final String KEY_TOLERANCE = "tolerance"; + private static final float DEFAULT_TOLERANCE = 0.10f; + + private static final long MIN_DISPLAY_PERIOD_MILLIS = 500; + private static final int WAVEFORM_SIZE = 400; + + private TextView mTextTolerance; + private SeekBar mFaderTolerance; + protected ExponentialTaper mTaperTolerance; + + private CheckBox mForceGlitchesBox; + private CheckBox mAutoScopeBox; + private WaveformView mWaveformView; + private LinearLayout mLayoutGlitch; + + + private NumberedRadioButtons mInputChannelBoxes; + private NumberedRadioButtons mOutputChannelBoxes; + private float[] mWaveform = new float[WAVEFORM_SIZE]; + private long mLastDisplayTime; + + private float mTolerance = DEFAULT_TOLERANCE; + + private SeekBar.OnSeekBarChangeListener mToleranceListener = new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + setToleranceProgress(progress); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + } + }; + + protected void setToleranceProgress(int progress) { + float tolerance = (float) mTaperTolerance.linearToExponential( + ((double)progress) / FADER_PROGRESS_MAX); + setTolerance(tolerance); + mTextTolerance.setText("Tolerance = " + String.format(Locale.getDefault(), "%5.3f", tolerance)); + } + + static class NumberedRadioButtons { + LinearLayout mRow; + RadioButton[] mRadioButtons; + + public interface SelectionListener { + void onSelected(int index); + } + + NumberedRadioButtons(Context context, int numBoxes, SelectionListener listener, String prompt) { + mRow = new LinearLayout(context); + mRow.setOrientation(LinearLayout.HORIZONTAL); + TextView textView = new TextView(context); + textView.setText(prompt); + mRow.addView(textView); + RadioGroup rg = new RadioGroup(context); + rg.setOrientation(LinearLayout.HORIZONTAL); + mRadioButtons = new RadioButton[numBoxes]; + for (int i = 0; i < numBoxes; i++) { + mRadioButtons[i] = new RadioButton(context); + mRadioButtons[i].setText("" + i); + mRadioButtons[i].setId(i); + rg.addView(mRadioButtons[i]); + } + mRow.addView(rg); + + //set listener to radio button group + rg.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(RadioGroup group, int checkedId) { + listener.onSelected(checkedId); + } + }); + + mRadioButtons[0].setChecked(true); + } + + public View getView() { + return mRow; + } + + public void setMaxEnabled(int max) { + max = Math.min(max, mRadioButtons.length); + for (int i = 0; i < mRadioButtons.length; i++) { + mRadioButtons[i].setEnabled(i < max); + } + mRadioButtons[0].setChecked(true); + } + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mTextTolerance = (TextView) findViewById(R.id.textTolerance); + mFaderTolerance = (SeekBar) findViewById(R.id.faderTolerance); + mTaperTolerance = new ExponentialTaper(0.0, 0.5, 100.0); + mFaderTolerance.setOnSeekBarChangeListener(mToleranceListener); + setToleranceFader(DEFAULT_TOLERANCE); + + mForceGlitchesBox = (CheckBox) findViewById(R.id.boxForceGlitch); + mAutoScopeBox = (CheckBox) findViewById(R.id.boxAutoScope); + mWaveformView = (WaveformView) findViewById(R.id.waveview_audio); + + mLayoutGlitch = (LinearLayout) findViewById(R.id.layoutGlitch); + mInputChannelBoxes = new NumberedRadioButtons(this, 8, + (int index) -> setInputChannel(index), "IN:"); + mLayoutGlitch.addView(mInputChannelBoxes.getView()); + mOutputChannelBoxes = new NumberedRadioButtons(this, 8, + (int index) -> setOutputChannel(index), "OUT:"); + mLayoutGlitch.addView(mOutputChannelBoxes.getView()); + } + + private void setToleranceFader(float tolerance) { + int progress = (int) Math.round((mTaperTolerance.exponentialToLinear( + tolerance) * FADER_PROGRESS_MAX)); + mFaderTolerance.setProgress(progress); + } + + @Override + protected void inflateActivity() { + setContentView(R.layout.activity_manual_glitches); + } + + void configureStreamsFromBundle(Bundle bundle) { + // Configure settings + StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration; + StreamConfiguration requestedOutConfig = mAudioOutTester.requestedConfiguration; + IntentBasedTestSupport.configureStreamsFromBundle(bundle, requestedInConfig, requestedOutConfig); + + // Extract custom parameters from the bundle. + float tolerance = bundle.getFloat(KEY_TOLERANCE, DEFAULT_TOLERANCE); + setToleranceFader(tolerance); + setTolerance(tolerance); + mTolerance = tolerance; + } + + @Override + public void giveAdvice(String s) { + mWaveformView.post(() -> { + mWaveformView.setMessage(s); + mWaveformView.invalidate(); + }); + } + + public void startAudioTest() throws IOException { + super.startAudioTest(); + + setToleranceProgress(mFaderTolerance.getProgress()); + int inChannels = mAudioInputTester.getCurrentAudioStream().getChannelCount(); + mInputChannelBoxes.setMaxEnabled(inChannels); + int outChannels = mAudioOutTester.getCurrentAudioStream().getChannelCount(); + mOutputChannelBoxes.setMaxEnabled(outChannels); + } + + @Override + public void startTestUsingBundle() { + configureStreamsFromBundle(mBundleFromIntent); + + int durationSeconds = IntentBasedTestSupport.getDurationSeconds(mBundleFromIntent); + int numBursts = mBundleFromIntent.getInt(KEY_BUFFER_BURSTS, VALUE_DEFAULT_BUFFER_BURSTS); + + try { + openStartAudioTestUI(); + int sizeFrames = mAudioOutTester.getCurrentAudioStream().getFramesPerBurst() * numBursts; + mAudioOutTester.getCurrentAudioStream().setBufferSizeInFrames(sizeFrames); + + // Schedule the end of the test. + Handler handler = new Handler(Looper.getMainLooper()); // UI thread + handler.postDelayed(new Runnable() { + @Override + public void run() { + stopAutomaticTest(); + } + }, durationSeconds * 1000); + } catch (IOException e) { + String report = "Open failed: " + e.getMessage(); + maybeWriteTestResult(report); + mTestRunningByIntent = false; + } finally { + mBundleFromIntent = null; + } + + } + + void stopAutomaticTest() { + String report = getCommonTestReport() + + String.format(Locale.getDefault(), "tolerance = %5.3f\n", mTolerance) + + mLastGlitchReport; + onStopAudioTest(null); + maybeWriteTestResult(report); + mTestRunningByIntent = false; + } + + // Only call from UI thread. + @Override + public void onTestBegan() { + mAutoScopeBox.setChecked(true); + mWaveformView.clearSampleData(); + mWaveformView.postInvalidate(); + super.onTestBegan(); + } + + // Called on UI thread + @Override + protected void onGlitchDetected() { + if (mAutoScopeBox.isChecked()) { + mAutoScopeBox.setChecked(false); // stop auto drawing of waveform + mLastDisplayTime = 0; // force draw first glitch + } + long now = System.currentTimeMillis(); + Log.i(TAG,"onGlitchDetected: glitch"); + if ((now - mLastDisplayTime) > MIN_DISPLAY_PERIOD_MILLIS) { + mLastDisplayTime = now; + int numSamples = getGlitch(mWaveform); + mWaveformView.setSampleData(mWaveform, 0, numSamples); + int glitchLength = getGlitchLength(); + int[] cursors = new int[glitchLength > 0 ? 2 : 1]; + int startOfGlitch = getSinePeriod(); + cursors[0] = startOfGlitch; + if (glitchLength > 0) { + cursors[1] = startOfGlitch + getGlitchLength(); + } + mWaveformView.setCursorData(cursors); + Log.i(TAG,"onGlitchDetected: glitch, numSamples = " + numSamples); + mWaveformView.postInvalidate(); + } + } + @Override + protected void maybeDisplayWaveform() { + if (!mAutoScopeBox.isChecked()) return; + long now = System.currentTimeMillis(); + if ((now - mLastDisplayTime) > MIN_DISPLAY_PERIOD_MILLIS) { + mLastDisplayTime = now; + int numSamples = getRecentSamples(mWaveform); + mWaveformView.setSampleData(mWaveform, 0, numSamples); + mWaveformView.setCursorData(null); + mWaveformView.postInvalidate(); + } + } + + private native int getGlitch(float[] mWaveform); + private native int getRecentSamples(float[] mWaveform); + + public void onForceGlitchClicked(View view) { + setForcedGlitchDuration(mForceGlitchesBox.isChecked() ? 100 : 0); + } +} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/MicrophoneInfoConverter.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/MicrophoneInfoConverter.java new file mode 100644 index 00000000..388bab95 --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/MicrophoneInfoConverter.java @@ -0,0 +1,82 @@ +package com.mobileer.oboetester; + +import android.annotation.TargetApi; +import android.media.MicrophoneInfo; +import android.os.Build; +import android.util.Pair; + +import java.util.List; +import java.util.Locale; + +public class MicrophoneInfoConverter { + @TargetApi(Build.VERSION_CODES.P) + static String convertDirectionality(int directionality) { + switch(directionality) { + case MicrophoneInfo.DIRECTIONALITY_BI_DIRECTIONAL: + return "Bidirectional"; + case MicrophoneInfo.DIRECTIONALITY_OMNI: + return "Omni"; + case MicrophoneInfo.DIRECTIONALITY_CARDIOID: + return "Cardioid"; + case MicrophoneInfo.DIRECTIONALITY_SUPER_CARDIOID: + return "SuperCardioid"; + case MicrophoneInfo.DIRECTIONALITY_HYPER_CARDIOID: + return "HyperCardioid"; + default: + return "Unknown"; + } + } + + @TargetApi(Build.VERSION_CODES.P) + static String convertLocation(int location) { + switch(location) { + case MicrophoneInfo.LOCATION_MAINBODY: + return "Main Body"; + case MicrophoneInfo.LOCATION_MAINBODY_MOVABLE: + return "Main Body Movable"; + case MicrophoneInfo.LOCATION_PERIPHERAL: + return "Peripheral"; + default: + return "Unknown"; + } + } + + @TargetApi(Build.VERSION_CODES.P) + static String convertCoordinates(MicrophoneInfo.Coordinate3F coordinates) { + if (coordinates == MicrophoneInfo.POSITION_UNKNOWN) return "Unknown"; + return String.format(Locale.getDefault(), "{ %6.4g, %5.3g, %5.3g }", + coordinates.x, coordinates.y, coordinates.z); + } + + @TargetApi(Build.VERSION_CODES.P) + public static String reportMicrophoneInfo(MicrophoneInfo micInfo) { + StringBuffer sb = new StringBuffer(); + sb.append("\n==== Microphone ========= " + micInfo.getId()); + sb.append("\nAddress : " + micInfo.getAddress()); + sb.append("\nDescription: " + micInfo.getDescription()); + sb.append("\nDirection : " + convertDirectionality(micInfo.getDirectionality())); + sb.append("\nLocation : " + convertLocation(micInfo.getLocation())); + sb.append("\nMinSPL : " + micInfo.getMinSpl()); + sb.append("\nMaxSPL : " + micInfo.getMaxSpl()); + sb.append("\nSensitivity: " + micInfo.getSensitivity()); + sb.append("\nGroup : " + micInfo.getGroup()); + sb.append("\nIndexInTheGroup: " + micInfo.getIndexInTheGroup()); + sb.append("\nOrientation: " + convertCoordinates(micInfo.getOrientation())); + sb.append("\nPosition : " + convertCoordinates(micInfo.getPosition())); + sb.append("\nType : " + micInfo.getType()); + + List> mapping = micInfo.getChannelMapping(); + if (mapping == null) { + throw new RuntimeException("MicrophoneInfo. getChannelMapping() returned null!"); + } + sb.append("\nChannelMapping: {"); + for (Pair pair : mapping) { + sb.append("[" + pair.first + "," + pair.second + "], "); + } + sb.append("}"); + + sb.append("\n"); + return sb.toString(); + } + +} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/MidiTapTester.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/MidiTapTester.java new file mode 100644 index 00000000..33e22665 --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/MidiTapTester.java @@ -0,0 +1,119 @@ +/* + * Copyright 2015 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. + */ +package com.mobileer.oboetester; + + +import android.media.midi.MidiDeviceService; +import android.media.midi.MidiReceiver; +import android.util.Log; + +import com.mobileer.miditools.MidiConstants; +import com.mobileer.miditools.MidiFramer; + +import java.io.IOException; +import java.util.ArrayList; + +/** + * Measure the latency of various output paths by playing a blip. + * Report the results back to the TestListeners. + */ +public class MidiTapTester extends MidiDeviceService { + // These must match the values in service_device_info.xml + public static final String PRODUCT_NAME = "MidiTapLatencyTester"; + public static final String MANUFACTURER_NAME = "Mobileer"; + + // Sometimes the service can be run without the MainActivity being run! + static { + // Must match name in CMakeLists.txt + System.loadLibrary("oboetester"); + } + + private ArrayList mListeners = new ArrayList(); + private MyMidiReceiver mReceiver = new MyMidiReceiver(); + private MidiFramer mMidiFramer = new MidiFramer(mReceiver); + + private static MidiTapTester mInstance; + + public static interface NoteListener { + public void onNoteOn(int pitch); + } + + /** + * This is a Service so it is only created when a client requests the service. + */ + public MidiTapTester() { + mInstance = this; + } + + public void addTestListener(NoteListener listener) { + mListeners.add(listener); + } + + public void removeTestListener(NoteListener listener) { + mListeners.remove(listener); + } + + @Override + public void onCreate() { + super.onCreate(); + } + + @Override + public void onDestroy() { + // do stuff here + super.onDestroy(); + } + + public static MidiTapTester getInstanceOrNull() { + return mInstance; + } + + class MyMidiReceiver extends MidiReceiver { + public void onSend(byte[] data, int offset, + int count, long timestamp) throws IOException { + // parse MIDI + byte command = (byte) (data[0] & 0x0F0); + if (command == MidiConstants.STATUS_NOTE_ON) { + if (data[2] == 0) { + noteOff(data[1]); + } else { + noteOn(data[1]); + } + } else if (command == MidiConstants.STATUS_NOTE_OFF) { + noteOff(data[1]); + } + Log.i(TapToToneActivity.TAG, "MIDI command = " + command); + } + } + + private void noteOn(byte b) { + fireNoteOn(b); + } + + private void fireNoteOn(byte pitch) { + for (NoteListener listener : mListeners) { + listener.onNoteOn(pitch); + } + } + + private void noteOff(byte b) {} + + @Override + public MidiReceiver[] onGetInputPortReceivers() { + return new MidiReceiver[]{mMidiFramer}; + } + +} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/MultiLineChart.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/MultiLineChart.java new file mode 100644 index 00000000..9b21e985 --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/MultiLineChart.java @@ -0,0 +1,254 @@ +package com.mobileer.oboetester; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.util.AttributeSet; +import android.view.View; + +import java.util.ArrayList; + +/** + * Draw a chart with multiple traces + */ +public class MultiLineChart extends View { + public static final int NUM_DATA_VALUES = 512; + private Paint mWavePaint; + private Paint mCursorPaint; + private int mBackgroundColor = 0xFFF0F0F0; + private int mLineColor = Color.RED; + private Paint mBackgroundPaint; + float[] mVertices = new float[4]; + + CircularFloatArray mXData = new CircularFloatArray(NUM_DATA_VALUES); + private ArrayList mTraceList = new ArrayList<>(); + + public MultiLineChart(Context context) { + super(context); + init(null, 0); + } + + public MultiLineChart(Context context, AttributeSet attrs) { + super(context, attrs); + init(attrs, 0); + } + + public MultiLineChart(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(attrs, defStyle); + } + + private void init(AttributeSet attrs, int defStyle) { + // Load attributes + final TypedArray a = getContext().obtainStyledAttributes( + attrs, R.styleable.MultiLineChart, defStyle, 0); + + mBackgroundColor = a.getColor( + R.styleable.MultiLineChart_backgroundColor, + mBackgroundColor); + mLineColor = a.getColor( + R.styleable.MultiLineChart_backgroundColor, + mLineColor); + + a.recycle(); + + mWavePaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mWavePaint.setColor(mLineColor); + + mCursorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mCursorPaint.setColor(Color.RED); + mCursorPaint.setStrokeWidth(3.0f); + + mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mBackgroundPaint.setColor(mBackgroundColor); + mBackgroundPaint.setStyle(Paint.Style.FILL); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + canvas.drawRect(0.0f, 0.0f, getWidth(), + getHeight(), mBackgroundPaint); + + for (Trace trace : mTraceList) { + drawTrace(canvas, trace); + } + } + + void drawTrace(Canvas canvas, Trace trace) { + // Determine bounds and XY conversion. + int numPoints = mXData.size(); + if (numPoints < 2) return; + // Allocate array for polyline. + int arraySize = (numPoints - 1) * 4; + if (arraySize > mVertices.length) { + mVertices = new float[arraySize]; + } + // Setup scaling. + float previousX = 0.0f; + float previousY = 0.0f; + float xMax = getXData(1); + float xRange = xMax - getXData(numPoints); + float yMin = trace.getMin(); + float yRange = trace.getMax() - yMin; + float width = getWidth(); + float height = getHeight(); + float xScaler = width / xRange; + float yScaler = height / yRange; + // Iterate through the available data. + int vertexIndex = 0; + for (int i = 1; i < numPoints; i++) { + float xData = getXData(i); + float yData = trace.get(i); + float xPos = width - ((xMax - xData) * xScaler); + float yPos = height - ((yData - yMin) * yScaler); + if (i > 1) { + // Each line segment requires 4 values! + mVertices[vertexIndex++] = previousX; + mVertices[vertexIndex++] = previousY; + mVertices[vertexIndex++] = xPos; + mVertices[vertexIndex++] = yPos; + } + previousX = xPos; + previousY = yPos; + } + canvas.drawLines(mVertices, 0, vertexIndex, trace.paint); + } + + public float getXData(int i) { + return mXData.get(i); + } + + public MultiLineChart.Trace createTrace(String name, int color, float min, float max) { + Trace trace = new Trace(name, color, NUM_DATA_VALUES, min, max); + mTraceList.add(trace); + return trace; + } + + public void update() { + post(new Runnable() { + public void run() { + postInvalidate(); + } + }); + } + + public void addX(float value) { + mXData.add(value); + } + + public void reset() { + mXData.clear(); + for (Trace trace : mTraceList) { + trace.reset(); + } + } + + public static class Trace { + private final String mName; + public Paint paint; + protected float mMin; + protected float mMax; + protected CircularFloatArray mData; + + private Trace(String name, int color, int numValues, float min, float max) { + mName = name; + mMin = min; + mMax = max; + mData = new CircularFloatArray(numValues); + + paint = new Paint(Paint.ANTI_ALIAS_FLAG); + paint.setColor(color); + paint.setStrokeWidth(3.0f); + } + + public void reset() { + mData.clear(); + } + + public void add(float value) { + mData.add(value); + // Take the hit here instead of when drawing. + mData.add(Math.min(mMax, Math.max(mMin, value))); + } + + public int size() { + return mData.size(); + } + + /** + * Fetch a previous value. A delayIndex of 1 will return the most recently written value. + * A delayIndex of 2 will return the previously written value; + * @param delayIndex positive index of previously written data + * @return old value + */ + public float get(int delayIndex) { + return mData.get(delayIndex); + } + public float getMax() { + return mMax; + } + public float getMin() { + return mMin; + } + + public void setMin(float min) { + mMin = min; + } + public void setMax(float max) { + mMax = max; + } + } + + // Use explicit type for performance reasons. + private static class CircularFloatArray { + private float[] mData; + private int mIndexMask; + private int mCursor; // next location to be written + + public CircularFloatArray(int numValuesPowerOf2) { + if ((numValuesPowerOf2 & (numValuesPowerOf2 - 1)) != 0) { + throw new IllegalArgumentException("numValuesPowerOf2 not 2^N, was " + numValuesPowerOf2); + } + mData = new float[numValuesPowerOf2]; + mIndexMask = numValuesPowerOf2 - 1; + } + + /** + * Add one value to the array. + * This may overwrite the oldest data. + * @param value + */ + public void add(float value) { + int index = mCursor & mIndexMask; + mData[index] = value; + mCursor++; + } + + /** + * Number of valid entries. + * @return + */ + public int size() { + return Math.min(mCursor, mData.length); + } + + /** + * Fetch a previous value. A delayIndex of 1 will return the most recently written value. + * A delayIndex of 2 will return the previously written value; + * @param delayIndex positive index of previously written data + * @return old value + */ + public float get(int delayIndex) { + int index = (mCursor - delayIndex) & mIndexMask; + return mData[index]; + } + + public void clear() { + mCursor = 0; + } + } +} \ No newline at end of file diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/NativeEngine.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/NativeEngine.java new file mode 100644 index 00000000..95129dba --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/NativeEngine.java @@ -0,0 +1,18 @@ +package com.mobileer.oboetester; + +public class NativeEngine { + + static native boolean isMMapSupported(); + + static native boolean isMMapExclusiveSupported(); + + static native void setWorkaroundsEnabled(boolean enabled); + + static native boolean areWorkaroundsEnabled(); + + static native int getCpuCount(); + + static native void setCpuAffinityMask(int mask); + + static native void setWorkloadReportingEnabled(boolean enabled); +} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/NativeSniffer.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/NativeSniffer.java new file mode 100644 index 00000000..222173d7 --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/NativeSniffer.java @@ -0,0 +1,61 @@ +/* + * Copyright 2018 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. + */ + +package com.mobileer.oboetester; + +import android.os.Handler; +import android.os.Looper; + +abstract class NativeSniffer implements Runnable { + public static final int SNIFFER_UPDATE_PERIOD_MSEC = 100; + public static final int SNIFFER_UPDATE_DELAY_MSEC = 200; + protected Handler mHandler = new Handler(Looper.getMainLooper()); // UI thread + protected volatile boolean mEnabled = true; + + @Override + public void run() { + if (mEnabled && !isComplete()) { + updateStatusText(); + mHandler.postDelayed(this, SNIFFER_UPDATE_PERIOD_MSEC); + } + } + + public void startSniffer() { + // Start the initial runnable task by posting through the handler + mEnabled = true; + mHandler.postDelayed(this, SNIFFER_UPDATE_DELAY_MSEC); + } + + public void stopSniffer() { + mEnabled = false; + if (mHandler != null) { + mHandler.removeCallbacks(this); + // Final update of the text. + mHandler.post(this); + } + } + + /** + * You can override this is if you want to control when sniffing is finished. + * @return true if finished + */ + public boolean isComplete() { + return false; + } + + public abstract void updateStatusText(); + +} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/OboeAudioInputStream.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/OboeAudioInputStream.java similarity index 94% rename from mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/OboeAudioInputStream.java rename to mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/OboeAudioInputStream.java index f89cfea5..09831216 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/OboeAudioInputStream.java +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/OboeAudioInputStream.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.sample.oboe.manualtest; +package com.mobileer.oboetester; class OboeAudioInputStream extends OboeAudioStream { diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/OboeAudioOutputStream.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/OboeAudioOutputStream.java similarity index 70% rename from mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/OboeAudioOutputStream.java rename to mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/OboeAudioOutputStream.java index 52f9796d..7a727db4 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/OboeAudioOutputStream.java +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/OboeAudioOutputStream.java @@ -14,29 +14,23 @@ * limitations under the License. */ -package com.google.sample.oboe.manualtest; +package com.mobileer.oboetester; /** * Native synthesizer and audio output. */ public class OboeAudioOutputStream extends OboeAudioStream { - // WARNING - must match order in strings.xml - public static final int TONE_TYPE_SAW_PING = 0; - public static final int TONE_TYPE_SINE = 1; - public static final int TONE_TYPE_IMPULSE = 2; - public static final int TONE_TYPE_SAWTOOTH = 3; - @Override public boolean isInput() { return false; } - public native void setToneEnabled(boolean enabled); - - public native void setToneType(int index); + public native void trigger(); public native void setChannelEnabled(int channelIndex, boolean enabled); public native void setSignalType(int type); + + public native void setAmplitude(float amplitude); } diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/OboeAudioStream.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/OboeAudioStream.java similarity index 60% rename from mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/OboeAudioStream.java rename to mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/OboeAudioStream.java index e30e2835..5c8b2109 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/OboeAudioStream.java +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/OboeAudioStream.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.sample.oboe.manualtest; +package com.mobileer.oboetester; import java.io.IOException; @@ -29,7 +29,7 @@ abstract class OboeAudioStream extends AudioStreamBase { @Override public void stopPlayback() throws IOException { int result = stopPlaybackNative(); - if (result < 0) { + if (result != 0) { throw new IOException("Stop Playback failed! result = " + result); } } @@ -39,19 +39,13 @@ abstract class OboeAudioStream extends AudioStreamBase { @Override public void startPlayback() throws IOException { int result = startPlaybackNative(); - if (result < 0) { + if (result != 0) { throw new IOException("Start Playback failed! result = " + result); } } public native int startPlaybackNative(); - // Write disabled because the synth is in native code. - @Override - public int write(float[] buffer, int offset, int length) { - return 0; - } - @Override public void open(StreamConfiguration requestedConfiguration, StreamConfiguration actualConfiguration, int bufferSizeInFrames) throws IOException { @@ -59,13 +53,16 @@ abstract class OboeAudioStream extends AudioStreamBase { int result = openNative(requestedConfiguration.getNativeApi(), requestedConfiguration.getSampleRate(), requestedConfiguration.getChannelCount(), + requestedConfiguration.getChannelMask(), requestedConfiguration.getFormat(), requestedConfiguration.getSharingMode(), requestedConfiguration.getPerformanceMode(), requestedConfiguration.getInputPreset(), + requestedConfiguration.getUsage(), + requestedConfiguration.getContentType(), + requestedConfiguration.getBufferCapacityInFrames(), requestedConfiguration.getDeviceId(), requestedConfiguration.getSessionId(), - requestedConfiguration.getFramesPerBurst(), requestedConfiguration.getChannelConversionAllowed(), requestedConfiguration.getFormatConversionAllowed(), requestedConfiguration.getRateConversionQuality(), @@ -74,7 +71,11 @@ abstract class OboeAudioStream extends AudioStreamBase { ); if (result < 0) { streamIndex = INVALID_STREAM_INDEX; - throw new IOException("Open failed! result = " + result); + String message = "Open " + + (isInput() ? "Input" : "Output") + + " failed! result = " + result + ", " + + StreamConfiguration.convertErrorToText(result); + throw new IOException(message); } else { streamIndex = result; } @@ -83,9 +84,12 @@ abstract class OboeAudioStream extends AudioStreamBase { actualConfiguration.setSharingMode(getSharingMode()); actualConfiguration.setPerformanceMode(getPerformanceMode()); actualConfiguration.setInputPreset(getInputPreset()); + actualConfiguration.setUsage(getUsage()); + actualConfiguration.setContentType(getContentType()); actualConfiguration.setFramesPerBurst(getFramesPerBurst()); actualConfiguration.setBufferCapacityInFrames(getBufferCapacityInFrames()); actualConfiguration.setChannelCount(getChannelCount()); + actualConfiguration.setChannelMask(getChannelMask()); actualConfiguration.setDeviceId(getDeviceId()); actualConfiguration.setSessionId(getSessionId()); actualConfiguration.setFormat(getFormat()); @@ -93,19 +97,25 @@ abstract class OboeAudioStream extends AudioStreamBase { actualConfiguration.setDirection(isInput() ? StreamConfiguration.DIRECTION_INPUT : StreamConfiguration.DIRECTION_OUTPUT); + actualConfiguration.setHardwareChannelCount(getHardwareChannelCount()); + actualConfiguration.setHardwareSampleRate(getHardwareSampleRate()); + actualConfiguration.setHardwareFormat(getHardwareFormat()); } private native int openNative( int nativeApi, int sampleRate, int channelCount, + int channelMask, int format, int sharingMode, int performanceMode, int inputPreset, + int usage, + int contentType, + int bufferCapacityInFrames, int deviceId, int sessionId, - int framesPerRead, boolean channelConversionAllowed, boolean formatConversionAllowed, int rateConversionQuality, @@ -133,123 +143,185 @@ abstract class OboeAudioStream extends AudioStreamBase { } private native int getBufferSizeInFrames(int streamIndex); - @Override - public boolean isThresholdSupported() { - return true; - } - @Override public int setBufferSizeInFrames(int thresholdFrames) { return setBufferSizeInFrames(streamIndex, thresholdFrames); } private native int setBufferSizeInFrames(int streamIndex, int thresholdFrames); + @Override + public void setPerformanceHintEnabled(boolean checked) { + setPerformanceHintEnabled(streamIndex, checked); + } + private native void setPerformanceHintEnabled(int streamIndex, boolean checked); + + @Override + public void setHearWorkload(boolean checked) { + setHearWorkload(streamIndex, checked); + } + private native void setHearWorkload(int streamIndex, boolean checked); + public int getNativeApi() { return getNativeApi(streamIndex); } - public native int getNativeApi(int streamIndex); + private native int getNativeApi(int streamIndex); @Override public int getFramesPerBurst() { return getFramesPerBurst(streamIndex); } - public native int getFramesPerBurst(int streamIndex); + private native int getFramesPerBurst(int streamIndex); public int getSharingMode() { return getSharingMode(streamIndex); } - public native int getSharingMode(int streamIndex); + private native int getSharingMode(int streamIndex); public int getPerformanceMode() { return getPerformanceMode(streamIndex); } - public native int getPerformanceMode(int streamIndex); + private native int getPerformanceMode(int streamIndex); public int getInputPreset() { return getInputPreset(streamIndex); } - public native int getInputPreset(int streamIndex); + private native int getInputPreset(int streamIndex); public int getSampleRate() { return getSampleRate(streamIndex); } - public native int getSampleRate(int streamIndex); + private native int getSampleRate(int streamIndex); public int getFormat() { return getFormat(streamIndex); } - public native int getFormat(int streamIndex); + private native int getFormat(int streamIndex); + + public int getUsage() { + return getUsage(streamIndex); + } + private native int getUsage(int streamIndex); + + public int getContentType() { + return getContentType(streamIndex); + } + private native int getContentType(int streamIndex); public int getChannelCount() { return getChannelCount(streamIndex); } - public native int getChannelCount(int streamIndex); + private native int getChannelCount(int streamIndex); + + public int getChannelMask() { + return getChannelMask(streamIndex); + } + private native int getChannelMask(int streamIndex); + + public int getHardwareChannelCount() { + return getHardwareChannelCount(streamIndex); + } + private native int getHardwareChannelCount(int streamIndex); + + public int getHardwareSampleRate() { + return getHardwareSampleRate(streamIndex); + } + private native int getHardwareSampleRate(int streamIndex); + + public int getHardwareFormat() { + return getHardwareFormat(streamIndex); + } + private native int getHardwareFormat(int streamIndex); public int getDeviceId() { return getDeviceId(streamIndex); } - public native int getDeviceId(int streamIndex); + private native int getDeviceId(int streamIndex); public int getSessionId() { return getSessionId(streamIndex); } - public native int getSessionId(int streamIndex); + private native int getSessionId(int streamIndex); + public boolean isMMap() { return isMMap(streamIndex); } - public native boolean isMMap(int streamIndex); + private native boolean isMMap(int streamIndex); @Override - public long getCallbackCount() { - return getCallbackCount(streamIndex); + public native long getCallbackCount(); // TODO Move to another class? + + @Override + public int getLastErrorCallbackResult() { + return getLastErrorCallbackResult(streamIndex); } - public native long getCallbackCount(int streamIndex); + private native int getLastErrorCallbackResult(int streamIndex); @Override public long getFramesWritten() { return getFramesWritten(streamIndex); } - public native long getFramesWritten(int streamIndex); + private native long getFramesWritten(int streamIndex); @Override public long getFramesRead() { return getFramesRead(streamIndex); } - public native long getFramesRead(int streamIndex); + private native long getFramesRead(int streamIndex); @Override public int getXRunCount() { return getXRunCount(streamIndex); } - public native int getXRunCount(int streamIndex); + private native int getXRunCount(int streamIndex); @Override public double getLatency() { - return getLatency(streamIndex); + return getTimestampLatency(streamIndex); } - public native double getLatency(int streamIndex); + private native double getTimestampLatency(int streamIndex); @Override - public double getCpuLoad() { + public float getCpuLoad() { return getCpuLoad(streamIndex); } - public native double getCpuLoad(int streamIndex); + private native float getCpuLoad(int streamIndex); @Override - public native void setWorkload(double workload); + public float getAndResetMaxCpuLoad() { + return getAndResetMaxCpuLoad(streamIndex); + } + private native float getAndResetMaxCpuLoad(int streamIndex); + + @Override + public int getAndResetCpuMask() { + return getAndResetCpuMask(streamIndex); + } + private native int getAndResetCpuMask(int streamIndex); + + @Override + public String getCallbackTimeStr() { + return getCallbackTimeString(); + } + public native String getCallbackTimeString(); + + @Override + public native void setWorkload(int workload); @Override public int getState() { return getState(streamIndex); } - public native int getState(int streamIndex); + private native int getState(int streamIndex); public static native void setCallbackReturnStop(boolean b); + public static native void setHangTimeMillis(int hangTimeMillis); + public static native void setUseCallback(boolean checked); public static native void setCallbackSize(int callbackSize); public static native int getOboeVersionNumber(); + } diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/RecorderActivity.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/RecorderActivity.java similarity index 95% rename from mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/RecorderActivity.java rename to mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/RecorderActivity.java index cc4ed736..48df2185 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/RecorderActivity.java +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/RecorderActivity.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.sample.oboe.manualtest; +package com.mobileer.oboetester; import android.os.Bundle; import android.view.View; @@ -54,10 +54,8 @@ public class RecorderActivity extends TestInputActivity { updateButtons(); } - @Override - protected void onStart() { - super.onStart(); - setActivityType(ACTIVITY_RECORD_PLAY); + int getActivityType() { + return ACTIVITY_RECORD_PLAY; } public void onStartRecording(View view) { diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/RoundTripLatencyActivity.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/RoundTripLatencyActivity.java new file mode 100644 index 00000000..9e323201 --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/RoundTripLatencyActivity.java @@ -0,0 +1,780 @@ +/* + * Copyright 2018 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. + */ + +package com.mobileer.oboetester; + +import static com.mobileer.oboetester.IntentBasedTestSupport.configureStreamsFromBundle; + +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.view.View; +import android.widget.Button; +import android.widget.TextView; + +import androidx.annotation.NonNull; + +import java.io.File; +import java.io.IOException; +import java.util.Locale; + +/** + * Activity to measure latency on a full duplex stream. + */ +public class RoundTripLatencyActivity extends AnalyzerActivity { + + // STATEs defined in LatencyAnalyzer.h + private static final int STATE_MEASURE_BACKGROUND = 0; + private static final int STATE_IN_PULSE = 1; + private static final int STATE_GOT_DATA = 2; + private final static String LATENCY_FORMAT = "%4.2f"; + // When I use 5.3g I only get one digit after the decimal point! + private final static String CONFIDENCE_FORMAT = "%5.3f"; + + private TextView mAnalyzerView; + private Button mMeasureButton; + private Button mAverageButton; + private Button mScanButton; + private Button mCancelButton; + private Button mShareButton; + private boolean mHasRecording = false; + + private int mBufferBursts = -1; + private int mInputBufferCapacityInBursts = -1; + private int mInputFramesPerBurst; + private int mOutputBufferCapacityInBursts = -1; + private int mOutputFramesPerBurst; + private int mActualBufferBursts; + private boolean mOutputIsMMapExclusive; + private boolean mInputIsMMapExclusive; + + private Handler mHandler = new Handler(Looper.getMainLooper()); // UI thread + + DoubleStatistics mTimestampLatencyStats = new DoubleStatistics(); // for single measurement + + protected abstract class MultipleLatencyTestRunner { + final static int AVERAGE_TEST_DELAY_MSEC = 1000; // arbitrary + boolean mActive; + String mLastReport = ""; + + abstract String onAnalyserDone(); + + public abstract void start(); + + public void clear() { + mActive = false; + mLastReport = ""; + } + + public void cancel() { + mActive = false; + } + + public boolean isActive() { + return mActive; + } + + public String getLastReport() { + return mLastReport; + } + } + + // Run the test several times and report the average latency. + protected class AverageLatencyTestRunner extends MultipleLatencyTestRunner { + private final static int AVERAGE_TEST_DELAY_MSEC = 1000; // arbitrary + private static final int GOOD_RUNS_REQUIRED = 5; // arbitrary + private static final int MAX_BAD_RUNS_ALLOWED = 5; // arbitrary + private int mBadCount = 0; // number of bad measurements + + DoubleStatistics mLatencies = new DoubleStatistics(); + DoubleStatistics mConfidences = new DoubleStatistics(); + DoubleStatistics mTimestampLatencies = new DoubleStatistics(); // for multiple measurements + + private int getGoodCount() { + return mLatencies.count(); + } + + // Called on UI thread. + @Override + String onAnalyserDone() { + String message; + boolean reschedule = false; + if (!mActive) { + message = ""; + } else if (getMeasuredResult() != 0) { + mBadCount++; + if (mBadCount > MAX_BAD_RUNS_ALLOWED) { + cancel(); + updateButtons(false); + message = "averaging cancelled due to error\n"; + } else { + message = "skipping this bad run, " + + mBadCount + " of " + MAX_BAD_RUNS_ALLOWED + " max\n"; + reschedule = true; + } + } else { + double latency = getMeasuredLatencyMillis(); + mLatencies.add(latency); + double confidence = getMeasuredConfidence(); + mConfidences.add(confidence); + + double timestampLatency = getTimestampLatencyMillis(); + if (timestampLatency > 0.0) { + mTimestampLatencies.add(timestampLatency); + } + if (getGoodCount() < GOOD_RUNS_REQUIRED) { + reschedule = true; + } else { + mActive = false; + updateButtons(false); + } + message = reportAverage(); + } + if (reschedule) { + mHandler.postDelayed(new Runnable() { + @Override + public void run() { + measureSingleLatency(); + } + }, AVERAGE_TEST_DELAY_MSEC); + } + return message; + } + + private String reportAverage() { + String message; + if (getGoodCount() == 0 || mConfidences.getSum() == 0.0) { + message = "num.iterations = " + getGoodCount() + "\n"; + } else { + final double mAverageConfidence = mConfidences.calculateMean(); + double meanLatency = mLatencies.calculateMean(); + double meanAbsoluteDeviation = mLatencies.calculateMeanAbsoluteDeviation(meanLatency); + double timestampLatencyMean = -1; + double timestampLatencyMAD = 0.0; + if (mTimestampLatencies.count() > 0) { + timestampLatencyMean = mTimestampLatencies.calculateMean(); + timestampLatencyMAD = + mTimestampLatencies.calculateMeanAbsoluteDeviation(timestampLatencyMean); + } + message = "average.latency.msec = " + + String.format(Locale.getDefault(), LATENCY_FORMAT, meanLatency) + "\n" + + "mean.absolute.deviation = " + + String.format(Locale.getDefault(), LATENCY_FORMAT, meanAbsoluteDeviation) + "\n" + + "average.confidence = " + + String.format(Locale.getDefault(), CONFIDENCE_FORMAT, mAverageConfidence) + "\n" + + "min.latency.msec = " + String.format(Locale.getDefault(), LATENCY_FORMAT, mLatencies.getMin()) + "\n" + + "max.latency.msec = " + String.format(Locale.getDefault(), LATENCY_FORMAT, mLatencies.getMax()) + "\n" + + "num.iterations = " + mLatencies.count() + "\n" + + "timestamp.latency.msec = " + + String.format(Locale.getDefault(), LATENCY_FORMAT, timestampLatencyMean) + "\n" + + "timestamp.latency.mad = " + + String.format(Locale.getDefault(), LATENCY_FORMAT, timestampLatencyMAD) + "\n"; + } + message += "num.failed = " + mBadCount + "\n"; + message += "\n"; // mark end of average report + mLastReport = message; + return message; + } + + // Called on UI thread. + @Override + public void start() { + mLatencies = new DoubleStatistics(); + mConfidences = new DoubleStatistics(); + mTimestampLatencies = new DoubleStatistics(); + mBadCount = 0; + mActive = true; + mLastReport = ""; + measureSingleLatency(); + } + + } + AverageLatencyTestRunner mAverageLatencyTestRunner = new AverageLatencyTestRunner(); + MultipleLatencyTestRunner mCurrentLatencyTestRunner = mAverageLatencyTestRunner; + + /** + * Search for a discontinuity in latency based on a number of bursts. + * Use binary subdivision search algorithm. + */ + protected static class BinaryDiscontinuityFinder { + public static final double MAX_ALLOWED_DEVIATION = 0.2; + // Run the test with various buffer sizes to detect DSP MMAP position errors. + private int mLowBufferBursts = 2; + private int mLowBufferLatency = -1; + private int mMiddleBufferBursts = -1; + private int mMiddleBufferLatency = -1; + private int mHighBufferBursts = -1; + private int mHighBufferLatency = -1; + private String mMessage = "---"; + + private static final int STATE_MEASURE_LOW = 1; + private static final int STATE_MEASURE_HIGH = 2; + private static final int STATE_MEASURE_MIDDLE = 3; + private static final int STATE_DONE = 4; + private int mState = STATE_MEASURE_LOW; + private int mFramesPerBurst; + + public static final int RESULT_CONTINUE= 1; + public static final int RESULT_OK = 0; + public static final int RESULT_DISCONTINUITY = -1; // DSP is reading from the wrong place. + public static final int RESULT_ERROR = -2; // Could not measure latency + public static final int RESULT_UNDEFINED = -3; // Could not measure latency + + public int getFramesPerBurst() { + return mFramesPerBurst; + } + + public void setFramesPerBurst(int framesPerBurst) { + mFramesPerBurst = framesPerBurst; + } + + public String getMessage() { + return mMessage; + } + + public static class Result { + public int code = RESULT_UNDEFINED; + public int numBursts = -1; + } + + /** + * @return Result object with number of bursts and a RESULT code + */ + Result onAnalyserDone(int latencyFrames, + double confidence, + int actualBufferBursts, + int capacityInBursts, + boolean isMMapExclusive) { + Result result = new Result(); + mMessage = "analyzing"; + if (!isMMapExclusive) { + mMessage = "skipped, not MMAP Exclusive"; + result.code = RESULT_OK; + return result; + } + result.code = RESULT_CONTINUE; + switch (mState) { + case STATE_MEASURE_LOW: + mLowBufferLatency = latencyFrames; + mLowBufferBursts = actualBufferBursts; + // Now we measure the high side. + mHighBufferBursts = capacityInBursts; + result.code = RESULT_CONTINUE; + result.numBursts = mHighBufferBursts; + mMessage = "checked low bufferSize"; + mState = STATE_MEASURE_HIGH; + break; + case STATE_MEASURE_HIGH: + mMessage = "checked high bufferSize"; + mHighBufferLatency = latencyFrames; + mHighBufferBursts = actualBufferBursts; + if (measureLatencyLinearity(mLowBufferBursts, + mLowBufferLatency, + mHighBufferBursts, + mHighBufferLatency) > MAX_ALLOWED_DEVIATION) { + mState = STATE_MEASURE_MIDDLE; + } else { + result.code = RESULT_OK; + mMessage = "DSP position looks good"; + mState = STATE_DONE; + } + break; + case STATE_MEASURE_MIDDLE: + mMiddleBufferLatency = latencyFrames; + mMiddleBufferBursts = actualBufferBursts; + // Check to see which side is bad. + if (confidence < 0.5) { + // We may have landed on the DSP so we got a scrambled result. + result.code = RESULT_DISCONTINUITY; + mMessage = "on top of DSP!"; + mState = STATE_DONE; + } else { + double deviationLow = measureLatencyLinearity( + mLowBufferBursts, + mLowBufferLatency, + mMiddleBufferBursts, + mMiddleBufferLatency); + double deviationHigh = measureLatencyLinearity( + mMiddleBufferBursts, + mMiddleBufferLatency, + mHighBufferBursts, + mHighBufferLatency); + if (deviationLow >= deviationHigh) { + // bottom half was bad so subdivide it + mHighBufferBursts = mMiddleBufferBursts; + mHighBufferLatency = mMiddleBufferLatency; + mMessage = "low half not linear"; + } else { + // top half was bad so subdivide it + mLowBufferBursts = mMiddleBufferBursts; + mLowBufferLatency = mMiddleBufferLatency; + mMessage = "high half not linear"; + } + } + break; + default: + break; + } + + if (result.code == RESULT_CONTINUE) { + if (mState == STATE_MEASURE_MIDDLE) { + if ((mHighBufferBursts - mLowBufferBursts) <= 1) { + result.code = RESULT_DISCONTINUITY; + mMessage = "ERROR - DSP position error between " + + mLowBufferBursts + " and " + + mHighBufferBursts + " bursts!"; + } else { + // Subdivide the remaining search space. + mMiddleBufferBursts = (mHighBufferBursts + mLowBufferBursts) / 2; + result.numBursts = mMiddleBufferBursts; + } + } + } else if (result.code == RESULT_OK) { + mMessage = "PASS - no discontinuity"; + } + return result; + } + + private double measureLatencyLinearity(int bufferBursts1, int bufferLatency1, + int bufferBursts2, int bufferLatency2) { + int bufferFrames1 = bufferBursts1 * mFramesPerBurst; + int bufferFrames2 = bufferBursts2 * mFramesPerBurst; + int expectedLatencyDifference = bufferFrames2 - bufferFrames1; + int actualLatencyDifference = bufferLatency2 - bufferLatency1; + return Math.abs(expectedLatencyDifference - actualLatencyDifference) + / (double) expectedLatencyDifference; + } + + private String reportResults(String prefix) { + String message; + message = prefix + "buffer.bursts.low = " + mLowBufferBursts + "\n"; + message += prefix + "latency.frames.low = " + mLowBufferLatency + "\n"; + message += prefix + "buffer.bursts.high = " + mHighBufferBursts + "\n"; + message += prefix + "latency.frames.high = " + mHighBufferLatency + "\n"; + message += prefix + "result = " + mMessage + "\n"; + return message; + } + } + + protected class ScanLatencyTestRunner extends MultipleLatencyTestRunner { + BinaryDiscontinuityFinder inputFinder; + BinaryDiscontinuityFinder outputFinder; + private static final int MAX_BAD_RUNS_ALLOWED = 5; // arbitrary + private int mBadCount = 0; // number of bad measurements + + private static final int STATE_SCANNING_OUTPUT = 0; + private static final int STATE_SCANNING_INPUT = 1; + private static final int STATE_DONE = 2; + private int mState = STATE_SCANNING_OUTPUT; + + // Called on UI thread after each single latency measurement is complete. + // It decides whether the series is complete or more measurements are needed. + // If more measurements are needed then it sets mBufferBursts for Output or mInputMarginBursts for Input + // It keeps moving the low and high sizes until it bounds the discontinuity within a single burst. + @Override + String onAnalyserDone() { + BinaryDiscontinuityFinder.Result result = new BinaryDiscontinuityFinder.Result(); + result.code = BinaryDiscontinuityFinder.RESULT_OK; + String message = ""; + + if (!mActive) { + message = "done"; + } else if (getMeasuredResult() != 0) { + mBadCount++; + if (mBadCount > MAX_BAD_RUNS_ALLOWED) { + cancel(); + result.code = BinaryDiscontinuityFinder.RESULT_ERROR; + updateButtons(false); + message = "scanning cancelled due to error, " + mBadCount + " bad runs\n"; + } else { + message = "skipping this bad run, " + + mBadCount + " of " + MAX_BAD_RUNS_ALLOWED + " max\n"; + result.numBursts = mActualBufferBursts; + } + } else { + switch (mState) { + case STATE_SCANNING_OUTPUT: + outputFinder.setFramesPerBurst(mOutputFramesPerBurst); + result = outputFinder.onAnalyserDone(getMeasuredLatency(), + getMeasuredConfidence(), + mActualBufferBursts, + mOutputBufferCapacityInBursts, + mOutputIsMMapExclusive); + mBufferBursts = result.numBursts; + mInputMarginBursts = 0; + break; + case STATE_SCANNING_INPUT: + inputFinder.setFramesPerBurst(mInputFramesPerBurst); + result = inputFinder.onAnalyserDone(getMeasuredLatency(), + getMeasuredConfidence(), + mInputMarginBursts, + mInputBufferCapacityInBursts, + mInputIsMMapExclusive); + mBufferBursts = -1; + mInputMarginBursts = Math.min(result.numBursts, + mInputBufferCapacityInBursts - 1); + break; + } + } + + if (result.code == BinaryDiscontinuityFinder.RESULT_CONTINUE) { + runAnotherTest(); + } else { + // We finished one series. + mBufferBursts = -1; + mInputMarginBursts = 0; + switch (mState) { + case STATE_SCANNING_OUTPUT: + // Finished an output series to start an input series. + mState = STATE_SCANNING_INPUT; + runAnotherTest(); + break; + case STATE_SCANNING_INPUT: + mActive = false; + updateButtons(false); + mState = STATE_DONE; + break; + } + } + message += reportResults(); + return message; + } + + private void runAnotherTest() { + mHandler.postDelayed(new Runnable() { + @Override + public void run() { + measureSingleLatency(); + } + }, AVERAGE_TEST_DELAY_MSEC); + } + + private String reportResults() { + String message = "test = check MMAP DSP position\n"; + message += outputFinder.reportResults("output."); + message += "\n"; // separator between in/out + message += inputFinder.reportResults("input."); + mLastReport = message; + return message; + } + + // Called on UI thread. + @Override + public void start() { + mBadCount = 0; + inputFinder = new BinaryDiscontinuityFinder(); + outputFinder = new BinaryDiscontinuityFinder(); + mState = STATE_SCANNING_OUTPUT; + mBufferBursts = 2; + mActive = true; + mLastReport = ""; + measureSingleLatency(); + } + } + ScanLatencyTestRunner mScanLatencyTestRunner = new ScanLatencyTestRunner(); + + // Periodically query the status of the stream. + protected class LatencySniffer { + private int mCounter = 0; + public static final int SNIFFER_UPDATE_PERIOD_MSEC = 150; + public static final int SNIFFER_UPDATE_DELAY_MSEC = 300; + public static final int SNIFFER_MAX_COUNTER = 30 * 1000 / SNIFFER_UPDATE_PERIOD_MSEC; + + // Display status info for the stream. + private Runnable runnableCode = new Runnable() { + @Override + public void run() { + double timestampLatency = -1.0; + int state = getAnalyzerState(); + if (state == STATE_MEASURE_BACKGROUND || state == STATE_IN_PULSE) { + timestampLatency = measureTimestampLatency(); + // Some configurations do not support input timestamps. + if (timestampLatency > 0) { + mTimestampLatencyStats.add(timestampLatency); + } + } + + String message; + if (isAnalyzerDone()) { + if (mCurrentLatencyTestRunner.isActive()) { + message = mCurrentLatencyTestRunner.onAnalyserDone(); + } else { + message = getResultString(); + } + File resultFile = onAnalyzerDone(); + if (resultFile != null) { + message = "result.file = " + resultFile.getAbsolutePath() + "\n" + message; + } + } else if (mCounter > SNIFFER_MAX_COUNTER) { + message = getProgressText(); + message += convertStateToString(getAnalyzerState()) + "\n"; + message += "TIMEOUT after " + mCounter + " loops!\n"; + } else { + message = getProgressText(); + message += "please wait... " + mCounter + "\n"; + message += convertStateToString(getAnalyzerState()) + "\n"; + // Repeat this runnable code block again. + mHandler.postDelayed(runnableCode, SNIFFER_UPDATE_PERIOD_MSEC); + } + setAnalyzerText(message); + mCounter++; + } + }; + + private void startSniffer() { + mCounter = 0; + // Start the initial runnable task by posting through the handler + mHandler.postDelayed(runnableCode, SNIFFER_UPDATE_DELAY_MSEC); + } + + private void stopSniffer() { + if (mHandler != null) { + mHandler.removeCallbacks(runnableCode); + } + } + } + + static String convertStateToString(int state) { + switch (state) { + case STATE_MEASURE_BACKGROUND: return "BACKGROUND"; + case STATE_IN_PULSE: return "RECORDING"; + case STATE_GOT_DATA: return "ANALYZING"; + default: return "DONE"; + } + } + + private String getProgressText() { + int progress = getAnalyzerProgress(); + int state = getAnalyzerState(); + int resetCount = getResetCount(); + String message = String.format(Locale.getDefault(), "progress = %d\nstate = %d\n#resets = %d\n", + progress, state, resetCount); + message += mCurrentLatencyTestRunner.getLastReport(); + return message; + } + + private File onAnalyzerDone() { + File resultFile = null; + if (mTestRunningByIntent) { + String report = getCommonTestReport(); + report += getResultString(); + resultFile = maybeWriteTestResult(report); + } + mTestRunningByIntent = false; + mHasRecording = true; + stopAudioTest(); + return resultFile; + } + + @NonNull + private String getResultString() { + int result = getMeasuredResult(); + int resetCount = getResetCount(); + double confidence = getMeasuredConfidence(); + String message = ""; + + message += String.format(Locale.getDefault(), "confidence = " + CONFIDENCE_FORMAT + "\n", confidence); + message += String.format(Locale.getDefault(), "result.text = %s\n", resultCodeToString(result)); + + // Only report valid latencies. + if (result == 0) { + int latencyFrames = getMeasuredLatency(); + double latencyMillis = getMeasuredLatencyMillis(); + int bufferSize = mAudioOutTester.getCurrentAudioStream().getBufferSizeInFrames(); + int latencyEmptyFrames = latencyFrames - bufferSize; + double latencyEmptyMillis = latencyEmptyFrames * 1000.0 / getSampleRate(); + message += String.format(Locale.getDefault(), "latency.msec = " + LATENCY_FORMAT + "\n", latencyMillis); + message += String.format(Locale.getDefault(), "latency.frames = %d\n", latencyFrames); + message += String.format(Locale.getDefault(), "latency.empty.msec = " + LATENCY_FORMAT + "\n", latencyEmptyMillis); + message += String.format(Locale.getDefault(), "latency.empty.frames = %d\n", latencyEmptyFrames); + } + + message += String.format(Locale.getDefault(), "rms.signal = %7.5f\n", getSignalRMS()); + message += String.format(Locale.getDefault(), "rms.noise = %7.5f\n", getBackgroundRMS()); + message += String.format(Locale.getDefault(), "correlation = " + CONFIDENCE_FORMAT + "\n", + getMeasuredCorrelation()); + double timestampLatency = getTimestampLatencyMillis(); + message += String.format(Locale.getDefault(), "timestamp.latency.msec = " + LATENCY_FORMAT + "\n", + timestampLatency); + if (mTimestampLatencyStats.count() > 0) { + message += String.format(Locale.getDefault(), "timestamp.latency.mad = " + LATENCY_FORMAT + "\n", + mTimestampLatencyStats.calculateMeanAbsoluteDeviation(timestampLatency)); + } + message += "timestamp.latency.count = " + mTimestampLatencyStats.count() + "\n"; + message += String.format(Locale.getDefault(), "reset.count = %d\n", resetCount); + message += String.format(Locale.getDefault(), "result = %d\n", result); + + return message; + } + + private LatencySniffer mLatencySniffer = new LatencySniffer(); + + double getMeasuredLatencyMillis() { + return getMeasuredLatency() * 1000.0 / getSampleRate(); + } + + double getTimestampLatencyMillis() { + if (mTimestampLatencyStats.count() == 0) return -1.0; + else return mTimestampLatencyStats.calculateMean(); + } + + native int getAnalyzerProgress(); + native int getMeasuredLatency(); + native double measureTimestampLatency(); + native double getMeasuredConfidence(); + native double getMeasuredCorrelation(); + native double getBackgroundRMS(); + native double getSignalRMS(); + + private void setAnalyzerText(String s) { + mAnalyzerView.setText(s); + } + + @Override + protected void inflateActivity() { + setContentView(R.layout.activity_rt_latency); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mMeasureButton = (Button) findViewById(R.id.button_measure); + mAverageButton = (Button) findViewById(R.id.button_average); + mScanButton = (Button) findViewById(R.id.button_scan); + mCancelButton = (Button) findViewById(R.id.button_cancel); + mShareButton = (Button) findViewById(R.id.button_share); + mShareButton.setEnabled(false); + mAnalyzerView = (TextView) findViewById(R.id.text_status); + updateEnabledWidgets(); + + hideSettingsViews(); + + mBufferSizeView.setFaderNormalizedProgress(0.0); // for lowest latency + + mCommunicationDeviceView = (CommunicationDeviceView) findViewById(R.id.comm_device_view); + } + + @Override + int getActivityType() { + return ACTIVITY_RT_LATENCY; + } + + @Override + protected void onStart() { + super.onStart(); + mHasRecording = false; + updateButtons(false); + } + + @Override + public void startTestUsingBundle() { + try { + StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration; + StreamConfiguration requestedOutConfig = mAudioOutTester.requestedConfiguration; + configureStreamsFromBundle(mBundleFromIntent, requestedInConfig, requestedOutConfig); + + mBufferBursts = mBundleFromIntent.getInt(IntentBasedTestSupport.KEY_BUFFER_BURSTS, mBufferBursts); + + onMeasure(null); + } finally { + mBundleFromIntent = null; + } + } + + @Override + protected void onStop() { + mLatencySniffer.stopSniffer(); + super.onStop(); + } + + public void onMeasure(View view) { + mCurrentLatencyTestRunner.clear(); + measureSingleLatency(); + } + + void updateButtons(boolean running) { + boolean busy = running || mCurrentLatencyTestRunner.isActive(); + mMeasureButton.setEnabled(!busy); + mAverageButton.setEnabled(!busy); + mScanButton.setEnabled(!busy && NativeEngine.isMMapExclusiveSupported()); + mCancelButton.setEnabled(running); + mShareButton.setEnabled(!busy && mHasRecording); + } + + private void measureSingleLatency() { + try { + openAudio(); + AudioStreamBase outputStream = mAudioOutTester.getCurrentAudioStream(); + mOutputFramesPerBurst = outputStream.getFramesPerBurst(); + mOutputBufferCapacityInBursts = outputStream.getBufferCapacityInFrames() / mOutputFramesPerBurst ; + mOutputIsMMapExclusive = mAudioOutTester.actualConfiguration.getSharingMode() + == StreamConfiguration.SHARING_MODE_EXCLUSIVE; + AudioStreamBase inputStream = mAudioInputTester.getCurrentAudioStream(); + mInputFramesPerBurst = inputStream.getFramesPerBurst(); + mInputBufferCapacityInBursts = inputStream.getBufferCapacityInFrames() / mInputFramesPerBurst ; + mInputIsMMapExclusive = mAudioInputTester.actualConfiguration.getSharingMode() + == StreamConfiguration.SHARING_MODE_EXCLUSIVE; + + if (mBufferBursts >= 0) { + int actualBufferSizeInFrames = outputStream.setBufferSizeInFrames(mOutputFramesPerBurst * mBufferBursts); + mActualBufferBursts = actualBufferSizeInFrames / mOutputFramesPerBurst; + // override buffer size fader + mBufferSizeView.setEnabled(false); + mBufferBursts = -1; + } + + startAudio(); + mTimestampLatencyStats = new DoubleStatistics(); + mLatencySniffer.startSniffer(); + updateButtons(true); + } catch (IOException e) { + showErrorToast(e.getMessage()); + } + } + + public void onAverage(View view) { + mCurrentLatencyTestRunner = mAverageLatencyTestRunner; + mCurrentLatencyTestRunner.start(); + } + + public void onScan(View view) { + mCurrentLatencyTestRunner = mScanLatencyTestRunner; + mCurrentLatencyTestRunner.start(); + } + + public void onCancel(View view) { + mCurrentLatencyTestRunner.cancel(); + stopAudioTest(); + } + + // Call on UI thread + public void stopAudioTest() { + mLatencySniffer.stopSniffer(); + stopAudio(); + closeAudio(); + updateButtons(false); + } + + @Override + String getWaveTag() { + return "rtlatency"; + } + + @Override + boolean isOutput() { + return false; + } +} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/StreamConfiguration.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/StreamConfiguration.java new file mode 100644 index 00000000..5e8e88b2 --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/StreamConfiguration.java @@ -0,0 +1,846 @@ +/* + * Copyright 2017 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. + */ + +package com.mobileer.oboetester; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; + +/** + * Container for the properties of a Stream. + * + * This can be used to build a stream, or as a base class for a Stream, + * or as a way to report the properties of a Stream. + */ + +public class StreamConfiguration { + public static final int UNSPECIFIED = 0; + + // These must match order in Spinner and in native code and in AAudio.h + public static final int NATIVE_API_UNSPECIFIED = 0; + public static final int NATIVE_API_OPENSLES = 1; + public static final int NATIVE_API_AAUDIO = 2; + + public static final int SHARING_MODE_EXCLUSIVE = 0; // must match AAUDIO + public static final int SHARING_MODE_SHARED = 1; // must match AAUDIO + + public static final int AUDIO_FORMAT_PCM_16 = 1; // must match AAUDIO + public static final int AUDIO_FORMAT_PCM_FLOAT = 2; // must match AAUDIO + public static final int AUDIO_FORMAT_PCM_24 = 3; // must match AAUDIO + public static final int AUDIO_FORMAT_PCM_32 = 4; // must match AAUDIO + public static final int AUDIO_FORMAT_IEC61937 = 5; // must match AAUDIO + + public static final int DIRECTION_OUTPUT = 0; // must match AAUDIO + public static final int DIRECTION_INPUT = 1; // must match AAUDIO + + public static final int SESSION_ID_NONE = -1; // must match AAUDIO + public static final int SESSION_ID_ALLOCATE = 0; // must match AAUDIO + + public static final int PERFORMANCE_MODE_NONE = 10; // must match AAUDIO + public static final int PERFORMANCE_MODE_POWER_SAVING = 11; // must match AAUDIO + public static final int PERFORMANCE_MODE_LOW_LATENCY = 12; // must match AAUDIO + + public static final int RATE_CONVERSION_QUALITY_NONE = 0; // must match Oboe + public static final int RATE_CONVERSION_QUALITY_FASTEST = 1; // must match Oboe + public static final int RATE_CONVERSION_QUALITY_LOW = 2; // must match Oboe + public static final int RATE_CONVERSION_QUALITY_MEDIUM = 3; // must match Oboe + public static final int RATE_CONVERSION_QUALITY_HIGH = 4; // must match Oboe + public static final int RATE_CONVERSION_QUALITY_BEST = 5; // must match Oboe + + public static final int STREAM_STATE_STARTING = 3; // must match Oboe + public static final int STREAM_STATE_STARTED = 4; // must match Oboe + + public static final int INPUT_PRESET_GENERIC = 1; // must match Oboe + public static final int INPUT_PRESET_CAMCORDER = 5; // must match Oboe + public static final int INPUT_PRESET_VOICE_RECOGNITION = 6; // must match Oboe + public static final int INPUT_PRESET_VOICE_COMMUNICATION = 7; // must match Oboe + public static final int INPUT_PRESET_UNPROCESSED = 9; // must match Oboe + public static final int INPUT_PRESET_VOICE_PERFORMANCE = 10; // must match Oboe + + public static final int ERROR_BASE = -900; // must match Oboe + public static final int ERROR_DISCONNECTED = -899; // must match Oboe + public static final int ERROR_ILLEGAL_ARGUMENT = -898; // must match Oboe + public static final int ERROR_INTERNAL = -896; // must match Oboe + public static final int ERROR_INVALID_STATE = -895; // must match Oboe + public static final int ERROR_INVALID_HANDLE = -892; // must match Oboe + public static final int ERROR_UNIMPLEMENTED = -890; // must match Oboe + public static final int ERROR_UNAVAILABLE = -889; // must match Oboe + public static final int ERROR_NO_FREE_HANDLES = -888; // must match Oboe + public static final int ERROR_NO_MEMORY = -887; // must match Oboe + public static final int ERROR_NULL = -886; // must match Oboe + public static final int ERROR_TIMEOUT = -885; // must match Oboe + public static final int ERROR_WOULD_BLOCK = -884; // must match Oboe + public static final int ERROR_INVALID_FORMAT = -883; // must match Oboe + public static final int ERROR_OUT_OF_RANGE = -882; // must match Oboe + public static final int ERROR_NO_SERVICE = -881; // must match Oboe + public static final int ERROR_INVALID_RATE = -880; // must match Oboe + public static final int ERROR_CLOSED = -869; // must match Oboe + public static final int ERROR_OK = 0; // must match Oboe + + public static final int USAGE_MEDIA = 1; + public static final int USAGE_VOICE_COMMUNICATION = 2; + public static final int USAGE_VOICE_COMMUNICATION_SIGNALLING = 3; + public static final int USAGE_ALARM = 4; + public static final int USAGE_NOTIFICATION = 5; + public static final int USAGE_NOTIFICATION_RINGTONE = 6; + public static final int USAGE_NOTIFICATION_EVENT = 10; + public static final int USAGE_ASSISTANCE_ACCESSIBILITY = 11; + public static final int USAGE_ASSISTANCE_NAVIGATION_GUIDANCE = 12; + public static final int USAGE_ASSISTANCE_SONIFICATION = 13; + public static final int USAGE_GAME = 14; + public static final int USAGE_ASSISTANT = 16; + + public static final int CONTENT_TYPE_SPEECH = 1; + public static final int CONTENT_TYPE_MUSIC = 2; + public static final int CONTENT_TYPE_MOVIE = 3; + public static final int CONTENT_TYPE_SONIFICATION = 4; + + public static final int CHANNEL_FRONT_LEFT = 1 << 0; + public static final int CHANNEL_FRONT_RIGHT = 1 << 1; + public static final int CHANNEL_FRONT_CENTER = 1 << 2; + public static final int CHANNEL_LOW_FREQUENCY = 1 << 3; + public static final int CHANNEL_BACK_LEFT = 1 << 4; + public static final int CHANNEL_BACK_RIGHT = 1 << 5; + public static final int CHANNEL_FRONT_LEFT_OF_CENTER = 1 << 6; + public static final int CHANNEL_FRONT_RIGHT_OF_CENTER = 1 << 7; + public static final int CHANNEL_BACK_CENTER = 1 << 8; + public static final int CHANNEL_SIDE_LEFT = 1 << 9; + public static final int CHANNEL_SIDE_RIGHT = 1 << 10; + public static final int CHANNEL_TOP_CENTER = 1 << 11; + public static final int CHANNEL_TOP_FRONT_LEFT = 1 << 12; + public static final int CHANNEL_TOP_FRONT_CENTER = 1 << 13; + public static final int CHANNEL_TOP_FRONT_RIGHT = 1 << 14; + public static final int CHANNEL_TOP_BACK_LEFT = 1 << 15; + public static final int CHANNEL_TOP_BACK_CENTER = 1 << 16; + public static final int CHANNEL_TOP_BACK_RIGHT = 1 << 17; + public static final int CHANNEL_TOP_SIDE_LEFT = 1 << 18; + public static final int CHANNEL_TOP_SIDE_RIGHT = 1 << 19; + public static final int CHANNEL_BOTTOM_FRONT_LEFT = 1 << 20; + public static final int CHANNEL_BOTTOM_FRONT_CENTER = 1 << 21; + public static final int CHANNEL_BOTTOM_FRONT_RIGHT = 1 << 22; + public static final int CHANNEL_LOW_FREQUENCY_2 = 1 << 23; + public static final int CHANNEL_FRONT_WIDE_LEFT = 1 << 24; + public static final int CHANNEL_FRONT_WIDE_RIGHT = 1 << 25; + + public static final int CHANNEL_MONO = CHANNEL_FRONT_LEFT; + public static final int CHANNEL_STEREO = CHANNEL_FRONT_LEFT | CHANNEL_FRONT_RIGHT; + public static final int CHANNEL_2POINT1 = CHANNEL_FRONT_LEFT | + CHANNEL_FRONT_RIGHT | + CHANNEL_LOW_FREQUENCY; + public static final int CHANNEL_TRI = CHANNEL_FRONT_LEFT | + CHANNEL_FRONT_RIGHT | + CHANNEL_FRONT_CENTER; + public static final int CHANNEL_TRI_BACK = CHANNEL_FRONT_LEFT | + CHANNEL_FRONT_RIGHT | + CHANNEL_BACK_CENTER; + public static final int CHANNEL_3POINT1 = CHANNEL_FRONT_LEFT | + CHANNEL_FRONT_RIGHT | + CHANNEL_FRONT_CENTER | + CHANNEL_LOW_FREQUENCY; + public static final int CHANNEL_2POINT0POINT2 = CHANNEL_FRONT_LEFT | + CHANNEL_FRONT_RIGHT | + CHANNEL_TOP_SIDE_LEFT | + CHANNEL_TOP_SIDE_RIGHT; + public static final int CHANNEL_2POINT1POINT2 = CHANNEL_2POINT0POINT2 | CHANNEL_LOW_FREQUENCY; + public static final int CHANNEL_3POINT0POINT2 = CHANNEL_FRONT_LEFT | + CHANNEL_FRONT_RIGHT | + CHANNEL_FRONT_CENTER | + CHANNEL_TOP_SIDE_LEFT | + CHANNEL_TOP_SIDE_RIGHT; + public static final int CHANNEL_3POINT1POINT2 = CHANNEL_3POINT0POINT2 | CHANNEL_LOW_FREQUENCY; + public static final int CHANNEL_QUAD = CHANNEL_FRONT_LEFT | + CHANNEL_FRONT_RIGHT | + CHANNEL_BACK_LEFT | + CHANNEL_BACK_RIGHT; + public static final int CHANNEL_QUAD_SIDE = CHANNEL_FRONT_LEFT | + CHANNEL_FRONT_RIGHT | + CHANNEL_SIDE_LEFT | + CHANNEL_SIDE_RIGHT; + public static final int CHANNEL_SURROUND = CHANNEL_FRONT_LEFT | + CHANNEL_FRONT_RIGHT | + CHANNEL_FRONT_CENTER | + CHANNEL_BACK_CENTER; + public static final int CHANNEL_PENTA = CHANNEL_QUAD | CHANNEL_FRONT_CENTER; + // aka 5POINT1_BACK + public static final int CHANNEL_5POINT1 = CHANNEL_FRONT_LEFT | + CHANNEL_FRONT_RIGHT | + CHANNEL_FRONT_CENTER | + CHANNEL_LOW_FREQUENCY | + CHANNEL_BACK_LEFT | + CHANNEL_BACK_RIGHT; + public static final int CHANNEL_5POINT1_SIDE = CHANNEL_FRONT_LEFT | + CHANNEL_FRONT_RIGHT | + CHANNEL_FRONT_CENTER | + CHANNEL_LOW_FREQUENCY | + CHANNEL_SIDE_LEFT | + CHANNEL_SIDE_RIGHT; + public static final int CHANNEL_6POINT1 = CHANNEL_FRONT_LEFT | + CHANNEL_FRONT_RIGHT | + CHANNEL_FRONT_CENTER | + CHANNEL_LOW_FREQUENCY | + CHANNEL_BACK_LEFT | + CHANNEL_BACK_RIGHT | + CHANNEL_BACK_CENTER; + public static final int CHANNEL_7POINT1 = CHANNEL_5POINT1 | + CHANNEL_SIDE_LEFT | + CHANNEL_SIDE_RIGHT; + public static final int CHANNEL_5POINT1POINT2 = CHANNEL_5POINT1 | + CHANNEL_TOP_SIDE_LEFT | + CHANNEL_TOP_SIDE_RIGHT; + public static final int CHANNEL_5POINT1POINT4 = CHANNEL_5POINT1 | + CHANNEL_TOP_FRONT_LEFT | + CHANNEL_TOP_FRONT_RIGHT | + CHANNEL_TOP_BACK_LEFT | + CHANNEL_TOP_BACK_RIGHT; + public static final int CHANNEL_7POINT1POINT2 = CHANNEL_7POINT1 | + CHANNEL_TOP_SIDE_LEFT | + CHANNEL_TOP_SIDE_RIGHT; + public static final int CHANNEL_7POINT1POINT4 = CHANNEL_7POINT1 | + CHANNEL_TOP_FRONT_LEFT | + CHANNEL_TOP_FRONT_RIGHT | + CHANNEL_TOP_BACK_LEFT | + CHANNEL_TOP_BACK_RIGHT; + public static final int CHANNEL_9POINT1POINT4 = CHANNEL_7POINT1POINT4 | + CHANNEL_FRONT_WIDE_LEFT | + CHANNEL_FRONT_WIDE_RIGHT; + public static final int CHANNEL_9POINT1POINT6 = CHANNEL_9POINT1POINT4 | + CHANNEL_TOP_SIDE_LEFT | + CHANNEL_TOP_SIDE_RIGHT; + public static final int CHANNEL_FRONT_BACK = CHANNEL_FRONT_CENTER | CHANNEL_BACK_CENTER; + + public static final int[] usages = { + USAGE_MEDIA, + USAGE_VOICE_COMMUNICATION, + USAGE_VOICE_COMMUNICATION_SIGNALLING, + USAGE_ALARM, + USAGE_NOTIFICATION, + USAGE_NOTIFICATION_RINGTONE, + USAGE_NOTIFICATION_EVENT, + USAGE_ASSISTANCE_ACCESSIBILITY, + USAGE_ASSISTANCE_NAVIGATION_GUIDANCE, + USAGE_ASSISTANCE_SONIFICATION, + USAGE_GAME, + USAGE_ASSISTANT}; + + public static final int[] contentTypes = { + CONTENT_TYPE_SPEECH, + CONTENT_TYPE_MUSIC, + CONTENT_TYPE_MOVIE, + CONTENT_TYPE_SONIFICATION}; + + public static final int[] channelMasks = { + CHANNEL_MONO, + CHANNEL_STEREO, + CHANNEL_2POINT1, + CHANNEL_TRI, + CHANNEL_TRI_BACK, + CHANNEL_3POINT1, + CHANNEL_2POINT0POINT2, + CHANNEL_2POINT1POINT2, + CHANNEL_3POINT0POINT2, + CHANNEL_3POINT1POINT2, + CHANNEL_QUAD, + CHANNEL_QUAD_SIDE, + CHANNEL_SURROUND, + CHANNEL_PENTA, + CHANNEL_5POINT1, + CHANNEL_5POINT1_SIDE, + CHANNEL_6POINT1, + CHANNEL_7POINT1, + CHANNEL_5POINT1POINT2, + CHANNEL_5POINT1POINT4, + CHANNEL_7POINT1POINT2, + CHANNEL_7POINT1POINT4, + CHANNEL_9POINT1POINT4, + CHANNEL_9POINT1POINT6, + CHANNEL_FRONT_BACK + }; + + private static HashMap mUsageStringToIntegerMap; + private static HashMap mContentTypeStringToIntegerMap; + private static HashMap mChannelMaskStringToIntegerMap; + private static List mChannelMaskStrings = new ArrayList<>(); + + private int mNativeApi; + private int mBufferCapacityInFrames; + private int mChannelCount; + private int mDeviceId; + private int mSessionId; + private int mDirection; // does not get reset + private int mFormat; + private int mSampleRate; + private int mSharingMode; + private int mPerformanceMode; + private boolean mFormatConversionAllowed; + private boolean mChannelConversionAllowed; + private int mRateConversionQuality; + private int mInputPreset; + private int mUsage; + private int mContentType; + private int mFramesPerBurst; + private boolean mMMap; + private int mChannelMask; + private int mHardwareChannelCount; + private int mHardwareSampleRate; + private int mHardwareFormat; + + public StreamConfiguration() { + reset(); + } + + static { + // Build map for Usage string-to-int conversion. + mUsageStringToIntegerMap = new HashMap(); + mUsageStringToIntegerMap.put(convertUsageToText(UNSPECIFIED), UNSPECIFIED); + for (int usage : usages) { + mUsageStringToIntegerMap.put(convertUsageToText(usage), usage); + } + + // Build map for Content Type string-to-int conversion. + mContentTypeStringToIntegerMap = new HashMap(); + mContentTypeStringToIntegerMap.put(convertContentTypeToText(UNSPECIFIED), UNSPECIFIED); + for (int contentType : contentTypes) { + mContentTypeStringToIntegerMap.put(convertContentTypeToText(contentType), contentType); + } + + // Build map for Channel Mask string-to-int conversion. + mChannelMaskStringToIntegerMap = new HashMap(); + String channelMaskStr = convertChannelMaskToText(UNSPECIFIED); + mChannelMaskStringToIntegerMap.put(channelMaskStr, UNSPECIFIED); + mChannelMaskStrings.add(channelMaskStr); + for (int channelMask : channelMasks) { + channelMaskStr = convertChannelMaskToText(channelMask); + mChannelMaskStringToIntegerMap.put(channelMaskStr, channelMask); + mChannelMaskStrings.add(channelMaskStr); + } + } + + public void reset() { + mNativeApi = NATIVE_API_UNSPECIFIED; + mBufferCapacityInFrames = UNSPECIFIED; + mChannelCount = UNSPECIFIED; + mChannelMask = UNSPECIFIED; + mDeviceId = UNSPECIFIED; + mSessionId = -1; + mFormat = AUDIO_FORMAT_PCM_FLOAT; + mSampleRate = UNSPECIFIED; + mSharingMode = SHARING_MODE_EXCLUSIVE; + mPerformanceMode = PERFORMANCE_MODE_LOW_LATENCY; + mInputPreset = INPUT_PRESET_VOICE_RECOGNITION; + mUsage = UNSPECIFIED; + mContentType = UNSPECIFIED; + mFormatConversionAllowed = false; + mChannelConversionAllowed = false; + mRateConversionQuality = RATE_CONVERSION_QUALITY_NONE; + mMMap = NativeEngine.isMMapSupported(); + mHardwareChannelCount = UNSPECIFIED; + mHardwareSampleRate = UNSPECIFIED; + mHardwareFormat = UNSPECIFIED; + } + + public int getFramesPerBurst() { + return mFramesPerBurst; + } + + public void setFramesPerBurst(int framesPerBurst) { + this.mFramesPerBurst = framesPerBurst; + } + + public int getBufferCapacityInFrames() { + return mBufferCapacityInFrames; + } + + public void setBufferCapacityInFrames(int bufferCapacityInFrames) { + this.mBufferCapacityInFrames = bufferCapacityInFrames; + } + + public int getFormat() { + return mFormat; + } + + public void setFormat(int format) { + this.mFormat = format; + } + + public int getDirection() { + return mDirection; + } + + public void setDirection(int direction) { + this.mDirection = direction; + } + + public int getPerformanceMode() { + return mPerformanceMode; + } + + public void setPerformanceMode(int performanceMode) { + this.mPerformanceMode = performanceMode; + } + + static String convertPerformanceModeToText(int performanceMode) { + switch(performanceMode) { + case PERFORMANCE_MODE_NONE: + return "NO"; + case PERFORMANCE_MODE_POWER_SAVING: + return "PS"; + case PERFORMANCE_MODE_LOW_LATENCY: + return "LL"; + default: + return "??"; + } + } + + public int getInputPreset() { return mInputPreset; } + public void setInputPreset(int inputPreset) { + this.mInputPreset = inputPreset; + } + + public int getUsage() { return mUsage; } + public void setUsage(int usage) { + this.mUsage = usage; + } + + public int getContentType() { return mContentType; } + public void setContentType(int contentType) { + this.mContentType = contentType; + } + + static String convertUsageToText(int usage) { + switch(usage) { + case UNSPECIFIED: + return "Unspecified"; + case USAGE_MEDIA: + return "Media"; + case USAGE_VOICE_COMMUNICATION: + return "VoiceComm"; + case USAGE_VOICE_COMMUNICATION_SIGNALLING: + return "VoiceCommSig"; + case USAGE_ALARM: + return "Alarm"; + case USAGE_NOTIFICATION: + return "Notification"; + case USAGE_NOTIFICATION_RINGTONE: + return "Ringtone"; + case USAGE_NOTIFICATION_EVENT: + return "Event"; + case USAGE_ASSISTANCE_ACCESSIBILITY: + return "Accessability"; + case USAGE_ASSISTANCE_NAVIGATION_GUIDANCE: + return "Navigation"; + case USAGE_ASSISTANCE_SONIFICATION: + return "Sonification"; + case USAGE_GAME: + return "Game"; + case USAGE_ASSISTANT: + return "Assistant"; + default: + return "?=" + usage; + } + } + + public static int convertTextToUsage(String text) { + return mUsageStringToIntegerMap.get(text); + } + + static String convertContentTypeToText(int contentType) { + switch(contentType) { + case UNSPECIFIED: + return "Unspecified"; + case CONTENT_TYPE_SPEECH: + return "Speech"; + case CONTENT_TYPE_MUSIC: + return "Music"; + case CONTENT_TYPE_MOVIE: + return "Movie"; + case CONTENT_TYPE_SONIFICATION: + return "Sonification"; + default: + return "?=" + contentType; + } + } + + public static int convertTextToContentType(String text) { + return mContentTypeStringToIntegerMap.get(text); + } + + public int getSharingMode() { + return mSharingMode; + } + + public void setSharingMode(int sharingMode) { + this.mSharingMode = sharingMode; + } + + static String convertSharingModeToText(int sharingMode) { + switch(sharingMode) { + case SHARING_MODE_SHARED: + return "SH"; + case SHARING_MODE_EXCLUSIVE: + return "EX"; + default: + return "??"; + } + } + + public static String convertFormatToText(int format) { + switch(format) { + case UNSPECIFIED: + return "Unspecified"; + case AUDIO_FORMAT_PCM_16: + return "I16"; + case AUDIO_FORMAT_PCM_24: + return "I24"; + case AUDIO_FORMAT_PCM_32: + return "I32"; + case AUDIO_FORMAT_PCM_FLOAT: + return "Float"; + case AUDIO_FORMAT_IEC61937: + return "IEC61937"; + default: + return "Invalid"; + } + } + + public static String convertNativeApiToText(int api) { + switch(api) { + case NATIVE_API_UNSPECIFIED: + return "Unspec"; + case NATIVE_API_AAUDIO: + return "AAudio"; + case NATIVE_API_OPENSLES: + return "OpenSL"; + default: + return "Invalid"; + } + } + + public static String convertChannelMaskToText(int channelMask) { + switch (channelMask) { + case UNSPECIFIED: + return "Unspecified"; + case CHANNEL_MONO: + return "Mono"; + case CHANNEL_STEREO: + return "Stereo"; + case CHANNEL_2POINT1: + return "2.1"; + case CHANNEL_TRI: + return "Tri"; + case CHANNEL_TRI_BACK: + return "TriBack"; + case CHANNEL_3POINT1: + return "3.1"; + case CHANNEL_2POINT0POINT2: + return "2.0.2"; + case CHANNEL_2POINT1POINT2: + return "2.1.2"; + case CHANNEL_3POINT0POINT2: + return "3.0.2"; + case CHANNEL_3POINT1POINT2: + return "3.1.2"; + case CHANNEL_QUAD: + return "Quad"; + case CHANNEL_QUAD_SIDE: + return "QuadSide"; + case CHANNEL_SURROUND: + return "Surround"; + case CHANNEL_PENTA: + return "Penta"; + case CHANNEL_5POINT1: + return "5.1"; + case CHANNEL_5POINT1_SIDE: + return "5.1Side"; + case CHANNEL_6POINT1: + return "6.1"; + case CHANNEL_7POINT1: + return "7.1"; + case CHANNEL_5POINT1POINT2: + return "5.1.2"; + case CHANNEL_5POINT1POINT4: + return "5.1.4"; + case CHANNEL_7POINT1POINT2: + return "7.1.2"; + case CHANNEL_7POINT1POINT4: + return "7.1.4"; + case CHANNEL_9POINT1POINT4: + return "9.1.4"; + case CHANNEL_9POINT1POINT6: + return "9.1.6"; + case CHANNEL_FRONT_BACK: + return "FrontBack"; + default: + return "?=" + Integer.toHexString(channelMask); + } + } + + static String convertRateConversionQualityToText(int quality) { + switch(quality) { + case RATE_CONVERSION_QUALITY_NONE: + return "None"; + case RATE_CONVERSION_QUALITY_FASTEST: + return "Fastest"; + case RATE_CONVERSION_QUALITY_LOW: + return "Low"; + case RATE_CONVERSION_QUALITY_MEDIUM: + return "Medium"; + case RATE_CONVERSION_QUALITY_HIGH: + return "High"; + case RATE_CONVERSION_QUALITY_BEST: + return "Best"; + default: + return "?=" + quality; + } + } + + public static int convertTextToChannelMask(String text) { + return mChannelMaskStringToIntegerMap.get(text); + } + + + public String dump() { + String prefix = (getDirection() == DIRECTION_INPUT) ? "in" : "out"; + StringBuffer message = new StringBuffer(); + message.append(String.format(Locale.getDefault(), "%s.channels = %d\n", prefix, mChannelCount)); + message.append(String.format(Locale.getDefault(), "%s.perf = %s\n", prefix, + convertPerformanceModeToText(mPerformanceMode).toLowerCase(Locale.getDefault()))); + if (getDirection() == DIRECTION_INPUT) { + message.append(String.format(Locale.getDefault(), "%s.preset = %s\n", prefix, + convertInputPresetToText(mInputPreset).toLowerCase(Locale.getDefault()))); + } else { + message.append(String.format(Locale.getDefault(), "%s.usage = %s\n", prefix, + convertUsageToText(mUsage).toLowerCase(Locale.getDefault()))); + message.append(String.format(Locale.getDefault(), "%s.contentType = %s\n", prefix, + convertContentTypeToText(mContentType).toLowerCase(Locale.getDefault()))); + } + message.append(String.format(Locale.getDefault(), "%s.sharing = %s\n", prefix, + convertSharingModeToText(mSharingMode).toLowerCase(Locale.getDefault()))); + message.append(String.format(Locale.getDefault(), "%s.api = %s\n", prefix, + convertNativeApiToText(getNativeApi()).toLowerCase(Locale.getDefault()))); + message.append(String.format(Locale.getDefault(), "%s.rate = %d\n", prefix, mSampleRate)); + message.append(String.format(Locale.getDefault(), "%s.device = %d\n", prefix, mDeviceId)); + message.append(String.format(Locale.getDefault(), "%s.mmap = %s\n", prefix, isMMap() ? "yes" : "no")); + message.append(String.format(Locale.getDefault(), "%s.rate.conversion.quality = %d\n", prefix, mRateConversionQuality)); + message.append(String.format(Locale.getDefault(), "%s.hardware.channels = %d\n", prefix, mHardwareChannelCount)); + message.append(String.format(Locale.getDefault(), "%s.hardware.sampleRate = %d\n", prefix, mHardwareSampleRate)); + message.append(String.format(Locale.getDefault(), "%s.hardware.format = %s\n", prefix, + convertFormatToText(mHardwareFormat).toLowerCase(Locale.getDefault()))); + return message.toString(); + } + + // text must match menu values + public static final String NAME_INPUT_PRESET_GENERIC = "Generic"; + public static final String NAME_INPUT_PRESET_CAMCORDER = "Camcorder"; + public static final String NAME_INPUT_PRESET_VOICE_RECOGNITION = "VoiceRec"; + public static final String NAME_INPUT_PRESET_VOICE_COMMUNICATION = "VoiceComm"; + public static final String NAME_INPUT_PRESET_UNPROCESSED = "Unprocessed"; + public static final String NAME_INPUT_PRESET_VOICE_PERFORMANCE = "Performance"; + + public static String convertInputPresetToText(int inputPreset) { + switch(inputPreset) { + case INPUT_PRESET_GENERIC: + return NAME_INPUT_PRESET_GENERIC; + case INPUT_PRESET_CAMCORDER: + return NAME_INPUT_PRESET_CAMCORDER; + case INPUT_PRESET_VOICE_RECOGNITION: + return NAME_INPUT_PRESET_VOICE_RECOGNITION; + case INPUT_PRESET_VOICE_COMMUNICATION: + return NAME_INPUT_PRESET_VOICE_COMMUNICATION; + case INPUT_PRESET_UNPROCESSED: + return NAME_INPUT_PRESET_UNPROCESSED; + case INPUT_PRESET_VOICE_PERFORMANCE: + return NAME_INPUT_PRESET_VOICE_PERFORMANCE; + default: + return "Invalid"; + } + } + + private static boolean matchInputPreset(String text, int preset) { + return convertInputPresetToText(preset).toLowerCase(Locale.getDefault()).equals(text); + } + + /** + * Case insensitive. + * @param text + * @return inputPreset, eg. INPUT_PRESET_CAMCORDER + */ + public static int convertTextToInputPreset(String text) { + text = text.toLowerCase(Locale.getDefault()); + if (matchInputPreset(text, INPUT_PRESET_GENERIC)) { + return INPUT_PRESET_GENERIC; + } else if (matchInputPreset(text, INPUT_PRESET_CAMCORDER)) { + return INPUT_PRESET_CAMCORDER; + } else if (matchInputPreset(text, INPUT_PRESET_VOICE_RECOGNITION)) { + return INPUT_PRESET_VOICE_RECOGNITION; + } else if (matchInputPreset(text, INPUT_PRESET_VOICE_COMMUNICATION)) { + return INPUT_PRESET_VOICE_COMMUNICATION; + } else if (matchInputPreset(text, INPUT_PRESET_UNPROCESSED)) { + return INPUT_PRESET_UNPROCESSED; + } else if (matchInputPreset(text, INPUT_PRESET_VOICE_PERFORMANCE)) { + return INPUT_PRESET_VOICE_PERFORMANCE; + } + return -1; + } + + public int getChannelCount() { + return mChannelCount; + } + + public void setChannelCount(int channelCount) { + this.mChannelCount = channelCount; + } + + public int getSampleRate() { + return mSampleRate; + } + + public void setSampleRate(int sampleRate) { + this.mSampleRate = sampleRate; + } + + public int getDeviceId() { + return mDeviceId; + } + + public void setDeviceId(int deviceId) { + this.mDeviceId = deviceId; + } + + public int getSessionId() { + return mSessionId; + } + + public void setSessionId(int sessionId) { + mSessionId = sessionId; + } + + public boolean isMMap() { + return mMMap; + } + + public void setMMap(boolean b) { mMMap = b; } + + public int getNativeApi() { + return mNativeApi; + } + + public void setNativeApi(int nativeApi) { + mNativeApi = nativeApi; + } + + public void setChannelConversionAllowed(boolean b) { mChannelConversionAllowed = b; } + + public boolean getChannelConversionAllowed() { + return mChannelConversionAllowed; + } + + public void setFormatConversionAllowed(boolean b) { + mFormatConversionAllowed = b; + } + + public boolean getFormatConversionAllowed() { + return mFormatConversionAllowed; + } + + public void setRateConversionQuality(int quality) { mRateConversionQuality = quality; } + + public int getRateConversionQuality() { + return mRateConversionQuality; + } + + public int getChannelMask() { + return mChannelMask; + } + + public void setChannelMask(int channelMask) { + this.mChannelMask = channelMask; + } + + public static List getAllChannelMasks() { + return mChannelMaskStrings; + } + + public int getHardwareChannelCount() { + return mHardwareChannelCount; + } + + public void setHardwareChannelCount(int hardwareChannelCount) { + this.mHardwareChannelCount = hardwareChannelCount; + } + + public int getHardwareSampleRate() { + return mHardwareSampleRate; + } + + public void setHardwareSampleRate(int hardwareSampleRate) { + this.mHardwareSampleRate = hardwareSampleRate; + } + + public int getHardwareFormat() { + return mHardwareFormat; + } + + public void setHardwareFormat(int hardwareFormat) { + this.mHardwareFormat = hardwareFormat; + } + + static String convertErrorToText(int error) { + switch (error) { + case ERROR_BASE: + return "ErrorBase"; + case ERROR_DISCONNECTED: + return "ErrorDisconnected"; + case ERROR_ILLEGAL_ARGUMENT: + return "ErrorIllegalArgument"; + case ERROR_INTERNAL: + return "ErrorInternal"; + case ERROR_INVALID_STATE: + return "ErrorInvalidState"; + case ERROR_INVALID_HANDLE: + return "ErrorInvalidHandle"; + case ERROR_UNIMPLEMENTED: + return "ErrorUnimplemented"; + case ERROR_UNAVAILABLE: + return "ErrorUnavailable"; + case ERROR_NO_FREE_HANDLES: + return "ErrorNoFreeHandles"; + case ERROR_NO_MEMORY: + return "ErrorNoMemory"; + case ERROR_NULL: + return "ErrorNull"; + case ERROR_TIMEOUT: + return "ErrorTimeout"; + case ERROR_WOULD_BLOCK: + return "ErrorWouldBlock"; + case ERROR_INVALID_FORMAT: + return "ErrorInvalidFormat"; + case ERROR_OUT_OF_RANGE: + return "ErrorOutOfRange"; + case ERROR_NO_SERVICE: + return "ErrorNoService"; + case ERROR_INVALID_RATE: + return "ErrorInvalidRate"; + case ERROR_CLOSED: + return "ErrorClosed"; + case ERROR_OK: + return "ErrorOk"; + default: + return "?=" + error; + } + } +} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/StreamConfigurationView.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/StreamConfigurationView.java new file mode 100644 index 00000000..3b7edca4 --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/StreamConfigurationView.java @@ -0,0 +1,653 @@ +/* + * Copyright 2017 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. + */ + +package com.mobileer.oboetester; + +import android.content.Context; +import android.media.AudioManager; +import android.media.audiofx.AcousticEchoCanceler; +import android.media.audiofx.AutomaticGainControl; +import android.media.audiofx.BassBoost; +import android.media.audiofx.LoudnessEnhancer; +import android.media.audiofx.NoiseSuppressor; +import android.util.AttributeSet; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.LinearLayout; +import android.widget.SeekBar; +import android.widget.Spinner; +import android.widget.TableRow; +import android.widget.TextView; + +import com.mobileer.audio_device.AudioDeviceListEntry; +import com.mobileer.audio_device.AudioDeviceSpinner; + +import java.util.Locale; + +/** + * View for Editing a requested StreamConfiguration + * and displaying the actual StreamConfiguration. + */ + +public class StreamConfigurationView extends LinearLayout { + private static final String TAG = "StreamConfigurationView"; + + protected Spinner mNativeApiSpinner; + private TextView mActualNativeApiView; + private TextView mActualDeviceIdView; + + private TextView mActualMMapView; + private CheckBox mRequestedMMapView; + private TextView mActualExclusiveView; + private TextView mActualPerformanceView; + private Spinner mPerformanceSpinner; + private CheckBox mRequestedExclusiveView; + private CheckBox mChannelConversionBox; + private CheckBox mFormatConversionBox; + private Spinner mChannelCountSpinner; + private TextView mActualChannelCountView; + private Spinner mChannelMaskSpinner; + private TextView mActualChannelMaskView; + private TextView mActualFormatView; + private Spinner mCapacitySpinner; + private TextView mActualCapacityView; + private TableRow mInputPresetTableRow; + private Spinner mInputPresetSpinner; + private TextView mActualInputPresetView; + + private TableRow mUsageTableRow; + private Spinner mUsageSpinner; + private TextView mActualUsageView; + + private TableRow mContentTypeTableRow; + private Spinner mContentTypeSpinner; + private TextView mActualContentTypeView; + + private Spinner mFormatSpinner; + private Spinner mSampleRateSpinner; + private Spinner mRateConversionQualitySpinner; + private TextView mActualSampleRateView; + private LinearLayout mHideableView; + + private AudioDeviceSpinner mDeviceSpinner; + private TextView mActualSessionIdView; + private CheckBox mRequestAudioEffect; + + private TextView mStreamInfoView; + private TextView mStreamStatusView; + private TextView mOptionExpander; + private String mHideSettingsText; + private String mShowSettingsText; + + private LinearLayout mInputEffectsLayout; + private LinearLayout mOutputEffectsLayout; + + private CheckBox mAutomaticGainControlCheckBox; + private CharSequence mAutomaticGainControlText; + private CheckBox mAcousticEchoCancelerCheckBox; + private CharSequence mAcousticEchoCancelerText; + private CheckBox mNoiseSuppressorCheckBox; + private CharSequence mNoiseSuppressorText; + private TextView mBassBoostTextView; + private SeekBar mBassBoostSeekBar; + private TextView mLoudnessEnhancerTextView; + private SeekBar mLoudnessEnhancerSeekBar; + + private boolean mIsChannelMaskLastSelected; + + private boolean misOutput; + + private BassBoost mBassBoost; + private LoudnessEnhancer mLoudnessEnhancer; + private AutomaticGainControl mAutomaticGainControl; + private AcousticEchoCanceler mAcousticEchoCanceler; + private NoiseSuppressor mNoiseSuppressor; + + // Create an anonymous implementation of OnClickListener + private View.OnClickListener mToggleListener = new View.OnClickListener() { + public void onClick(View v) { + if (mHideableView.isShown()) { + hideSettingsView(); + } else { + showSettingsView(); + } + } + }; + + public static String yesOrNo(boolean b) { + return b ? "YES" : "NO"; + } + + private void updateSettingsViewText() { + if (mHideableView.isShown()) { + mOptionExpander.setText(mHideSettingsText); + } else { + mOptionExpander.setText(mShowSettingsText); + } + } + + public void showSettingsView() { + mHideableView.setVisibility(View.VISIBLE); + updateSettingsViewText(); + } + + public void hideSampleRateMenu() { + if (mSampleRateSpinner != null) { + mSampleRateSpinner.setVisibility(View.GONE); + } + } + + public void hideSettingsView() { + mHideableView.setVisibility(View.GONE); + updateSettingsViewText(); + } + + public StreamConfigurationView(Context context) { + super(context); + initializeViews(context); + } + + public StreamConfigurationView(Context context, AttributeSet attrs) { + super(context, attrs); + initializeViews(context); + } + + public StreamConfigurationView(Context context, + AttributeSet attrs, + int defStyle) { + super(context, attrs, defStyle); + initializeViews(context); + } + + /** + * Inflates the views in the layout. + * + * @param context + * the current context for the view. + */ + private void initializeViews(Context context) { + LayoutInflater inflater = (LayoutInflater) context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.stream_config, this); + + mHideSettingsText = getResources().getString(R.string.hint_hide_settings); + mShowSettingsText = getResources().getString(R.string.hint_show_settings); + + mHideableView = (LinearLayout) findViewById(R.id.hideableView); + + mOptionExpander = (TextView) findViewById(R.id.toggle_stream_config); + mOptionExpander.setOnClickListener(mToggleListener); + + mNativeApiSpinner = (Spinner) findViewById(R.id.spinnerNativeApi); + mNativeApiSpinner.setSelection(StreamConfiguration.NATIVE_API_UNSPECIFIED); + + mActualNativeApiView = (TextView) findViewById(R.id.actualNativeApi); + + mActualDeviceIdView = (TextView) findViewById(R.id.actualDeviceId); + + mChannelConversionBox = (CheckBox) findViewById(R.id.checkChannelConversion); + + mFormatConversionBox = (CheckBox) findViewById(R.id.checkFormatConversion); + + mActualMMapView = (TextView) findViewById(R.id.actualMMap); + mRequestedMMapView = (CheckBox) findViewById(R.id.requestedMMapEnable); + boolean mmapSupported = NativeEngine.isMMapSupported(); + mRequestedMMapView.setEnabled(mmapSupported); + mRequestedMMapView.setChecked(mmapSupported); + + mActualExclusiveView = (TextView) findViewById(R.id.actualExclusiveMode); + mRequestedExclusiveView = (CheckBox) findViewById(R.id.requestedExclusiveMode); + + boolean mmapExclusiveSupported = NativeEngine.isMMapExclusiveSupported(); + mRequestedExclusiveView.setEnabled(mmapExclusiveSupported); + mRequestedExclusiveView.setChecked(mmapExclusiveSupported); + + mActualSessionIdView = (TextView) findViewById(R.id.sessionId); + mRequestAudioEffect = (CheckBox) findViewById(R.id.requestAudioEffect); + + mOutputEffectsLayout = (LinearLayout) findViewById(R.id.outputEffects); + mInputEffectsLayout = (LinearLayout) findViewById(R.id.inputEffects); + + mRequestAudioEffect.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + onRequestAudioEffectClicked(((CheckBox) view).isChecked()); + } + }); + + mAutomaticGainControlCheckBox = (CheckBox) findViewById(R.id.checkBoxAutomaticGainControl); + mAcousticEchoCancelerCheckBox = (CheckBox) findViewById(R.id.checkBoxAcousticEchoCanceler); + mNoiseSuppressorCheckBox = (CheckBox) findViewById(R.id.checkBoxNoiseSuppressor); + mBassBoostTextView = (TextView) findViewById(R.id.textBassBoost); + mBassBoostSeekBar = (SeekBar) findViewById(R.id.seekBarBassBoost); + mLoudnessEnhancerTextView = (TextView) findViewById(R.id.textLoudnessEnhancer); + mLoudnessEnhancerSeekBar = (SeekBar) findViewById(R.id.seekBarLoudnessEnhancer); + + mAutomaticGainControlCheckBox.setEnabled(AutomaticGainControl.isAvailable()); + mAutomaticGainControlText = mAutomaticGainControlCheckBox.getText(); + mAcousticEchoCancelerCheckBox.setEnabled(AcousticEchoCanceler.isAvailable()); + mAcousticEchoCancelerText = mAcousticEchoCancelerCheckBox.getText(); + mNoiseSuppressorCheckBox.setEnabled(NoiseSuppressor.isAvailable()); + mNoiseSuppressorText = mNoiseSuppressorCheckBox.getText(); + + mBassBoostSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + onBassBoostSeekBarChanged(progress); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + } + }); + + mLoudnessEnhancerSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + onLoudnessEnhancerSeekBarChanged(progress); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + } + }); + + mAutomaticGainControlCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + onAutomaticGainControlCheckBoxChanged(isChecked); + } + }); + + mAcousticEchoCancelerCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + onAcousticEchoCancelerCheckBoxChanged(isChecked); + } + }); + mNoiseSuppressorCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + onNoiseSuppressorCheckBoxChanged(isChecked); + } + }); + + mActualSampleRateView = (TextView) findViewById(R.id.actualSampleRate); + mSampleRateSpinner = (Spinner) findViewById(R.id.spinnerSampleRate); + mActualChannelCountView = (TextView) findViewById(R.id.actualChannelCount); + mChannelCountSpinner = (Spinner) findViewById(R.id.spinnerChannelCount); + mChannelCountSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView adapterView, View view, int i, long l) { + onChannelCountSpinnerSelected(); + } + + @Override + public void onNothingSelected(AdapterView adapterView) { + // no-op + } + }); + mActualFormatView = (TextView) findViewById(R.id.actualAudioFormat); + mFormatSpinner = (Spinner) findViewById(R.id.spinnerFormat); + + mActualCapacityView = (TextView) findViewById(R.id.actualCapacity); + mCapacitySpinner = (Spinner) findViewById(R.id.spinnerCapacity); + + mRateConversionQualitySpinner = (Spinner) findViewById(R.id.spinnerSRCQuality); + + mActualPerformanceView = (TextView) findViewById(R.id.actualPerformanceMode); + mPerformanceSpinner = (Spinner) findViewById(R.id.spinnerPerformanceMode); + mPerformanceSpinner.setSelection(StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY + - StreamConfiguration.PERFORMANCE_MODE_NONE); + + mInputPresetTableRow = (TableRow) findViewById(R.id.rowInputPreset); + mActualInputPresetView = (TextView) findViewById(R.id.actualInputPreset); + mInputPresetSpinner = (Spinner) findViewById(R.id.spinnerInputPreset); + mInputPresetSpinner.setSelection(2); // TODO need better way to select voice recording default + + mUsageTableRow = (TableRow) findViewById(R.id.rowUsage); + mActualUsageView = (TextView) findViewById(R.id.actualUsage); + mUsageSpinner = (Spinner) findViewById(R.id.spinnerUsage); + + mContentTypeTableRow = (TableRow) findViewById(R.id.rowContentType); + mActualContentTypeView = (TextView) findViewById(R.id.actualContentType); + mContentTypeSpinner = (Spinner) findViewById(R.id.spinnerContentType); + + mStreamInfoView = (TextView) findViewById(R.id.streamInfo); + + mStreamStatusView = (TextView) findViewById(R.id.statusView); + + mDeviceSpinner = (AudioDeviceSpinner) findViewById(R.id.devices_spinner); + + mActualChannelMaskView = (TextView) findViewById(R.id.actualChannelMask); + mChannelMaskSpinner = (Spinner) findViewById(R.id.spinnerChannelMask); + ArrayAdapter channelMaskSpinnerArrayAdapter = new ArrayAdapter(context, + android.R.layout.simple_spinner_item, + StreamConfiguration.getAllChannelMasks()); + mChannelMaskSpinner.setAdapter(channelMaskSpinnerArrayAdapter); + mChannelMaskSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView adapterView, View view, int i, long l) { + onChannelMaskSpinnerSelected(); + } + + @Override + public void onNothingSelected(AdapterView adapterView) { + // no-op + } + }); + + showSettingsView(); + } + + public void setOutput(boolean output) { + misOutput = output; + String ioText; + if (output) { + mDeviceSpinner.setDirectionType(AudioManager.GET_DEVICES_OUTPUTS); + ioText = "OUTPUT"; + } else { + mDeviceSpinner.setDirectionType(AudioManager.GET_DEVICES_INPUTS); + ioText = "INPUT"; + } + mHideSettingsText = getResources().getString(R.string.hint_hide_settings) + " - " + ioText; + mShowSettingsText = getResources().getString(R.string.hint_show_settings) + " - " + ioText; + updateSettingsViewText(); + + // Don't show InputPresets for output streams. + mInputPresetTableRow.setVisibility(output ? View.GONE : View.VISIBLE); + // Don't show Usage and Content Type for input streams. + mUsageTableRow.setVisibility(output ? View.VISIBLE : View.GONE); + mContentTypeTableRow.setVisibility(output ? View.VISIBLE : View.GONE); + } + + public void applyToModel(StreamConfiguration config) { + // Menu position matches actual enum value for these properties. + config.setNativeApi(mNativeApiSpinner.getSelectedItemPosition()); + config.setFormat(mFormatSpinner.getSelectedItemPosition()); + config.setRateConversionQuality(mRateConversionQualitySpinner.getSelectedItemPosition()); + + int id = ((AudioDeviceListEntry) mDeviceSpinner.getSelectedItem()).getId(); + config.setDeviceId(id); + + String text = mSampleRateSpinner.getSelectedItem().toString(); + int sampleRate = Integer.parseInt(text); + config.setSampleRate(sampleRate); + + text = mInputPresetSpinner.getSelectedItem().toString(); + int inputPreset = StreamConfiguration.convertTextToInputPreset(text); + config.setInputPreset(inputPreset); + + text = mUsageSpinner.getSelectedItem().toString(); + int usage = StreamConfiguration.convertTextToUsage(text); + config.setUsage(usage); + + text = mContentTypeSpinner.getSelectedItem().toString(); + int contentType = StreamConfiguration.convertTextToContentType(text); + config.setContentType(contentType); + + // The corresponding channel count of the selected channel mask may be different from + // the selected channel count, the last selected will be respected. + if (mIsChannelMaskLastSelected) { + text = mChannelMaskSpinner.getSelectedItem().toString(); + int channelMask = StreamConfiguration.convertTextToChannelMask(text); + config.setChannelMask(channelMask); + config.setChannelCount(0); + Log.d(TAG, String.format(Locale.getDefault(), "Set channel mask as %s(%#x)", text, channelMask)); + } else { + config.setChannelCount(mChannelCountSpinner.getSelectedItemPosition()); + config.setChannelMask(StreamConfiguration.UNSPECIFIED); + Log.d(TAG, "Set channel count as " + mChannelCountSpinner.getSelectedItemPosition()); + } + + text = mCapacitySpinner.getSelectedItem().toString(); + int bufferCapacity = Integer.parseInt(text); + config.setBufferCapacityInFrames(bufferCapacity); + + config.setMMap(mRequestedMMapView.isChecked()); + config.setChannelConversionAllowed(mChannelConversionBox.isChecked()); + config.setFormatConversionAllowed(mFormatConversionBox.isChecked()); + config.setSharingMode(mRequestedExclusiveView.isChecked() + ? StreamConfiguration.SHARING_MODE_EXCLUSIVE + : StreamConfiguration.SHARING_MODE_SHARED); + config.setSessionId(mRequestAudioEffect.isChecked() + ? StreamConfiguration.SESSION_ID_ALLOCATE + : StreamConfiguration.SESSION_ID_NONE); + + config.setPerformanceMode(mPerformanceSpinner.getSelectedItemPosition() + + StreamConfiguration.PERFORMANCE_MODE_NONE); + } + + public void setChildrenEnabled(boolean enabled) { + mNativeApiSpinner.setEnabled(enabled); + mPerformanceSpinner.setEnabled(enabled); + mRequestedMMapView.setEnabled(enabled && NativeEngine.isMMapSupported()); + mRequestedExclusiveView.setEnabled(enabled && NativeEngine.isMMapExclusiveSupported()); + mChannelConversionBox.setEnabled(enabled); + mFormatConversionBox.setEnabled(enabled); + mChannelCountSpinner.setEnabled(enabled); + mChannelMaskSpinner.setEnabled(enabled); + mCapacitySpinner.setEnabled(enabled); + mInputPresetSpinner.setEnabled(enabled); + mUsageSpinner.setEnabled(enabled); + mContentTypeSpinner.setEnabled(enabled); + mFormatSpinner.setEnabled(enabled); + mSampleRateSpinner.setEnabled(enabled); + mRateConversionQualitySpinner.setEnabled(enabled); + mDeviceSpinner.setEnabled(enabled); + mRequestAudioEffect.setEnabled(enabled); + } + + // This must be called on the UI thread. + void updateDisplay(StreamConfiguration actualConfiguration) { + int value; + + value = actualConfiguration.getNativeApi(); + mActualNativeApiView.setText(StreamConfiguration.convertNativeApiToText(value)); + + value = actualConfiguration.getDeviceId(); + mActualDeviceIdView.setText(String.valueOf(value)); + + mActualMMapView.setText(yesOrNo(actualConfiguration.isMMap())); + int sharingMode = actualConfiguration.getSharingMode(); + boolean isExclusive = (sharingMode == StreamConfiguration.SHARING_MODE_EXCLUSIVE); + mActualExclusiveView.setText(yesOrNo(isExclusive)); + + value = actualConfiguration.getPerformanceMode(); + mActualPerformanceView.setText(StreamConfiguration.convertPerformanceModeToText(value)); + mActualPerformanceView.requestLayout(); + + value = actualConfiguration.getFormat(); + mActualFormatView.setText(StreamConfiguration.convertFormatToText(value)); + mActualFormatView.requestLayout(); + + value = actualConfiguration.getInputPreset(); + mActualInputPresetView.setText(StreamConfiguration.convertInputPresetToText(value)); + mActualInputPresetView.requestLayout(); + + value = actualConfiguration.getUsage(); + mActualUsageView.setText(StreamConfiguration.convertUsageToText(value)); + mActualUsageView.requestLayout(); + + value = actualConfiguration.getContentType(); + mActualContentTypeView.setText(StreamConfiguration.convertContentTypeToText(value)); + mActualContentTypeView.requestLayout(); + + mActualChannelCountView.setText(actualConfiguration.getChannelCount() + ""); + mActualSampleRateView.setText(actualConfiguration.getSampleRate() + ""); + mActualSessionIdView.setText("S#: " + actualConfiguration.getSessionId()); + value = actualConfiguration.getChannelMask(); + mActualChannelMaskView.setText(StreamConfiguration.convertChannelMaskToText(value)); + mActualCapacityView.setText(actualConfiguration.getBufferCapacityInFrames() + ""); + + boolean isMMap = actualConfiguration.isMMap(); + + String msg = ""; + msg += "burst = " + actualConfiguration.getFramesPerBurst(); + msg += ", devID = " + actualConfiguration.getDeviceId(); + msg += ", " + (actualConfiguration.isMMap() ? "MMAP" : "Legacy"); + msg += (isMMap ? ", " + StreamConfiguration.convertSharingModeToText(sharingMode) : ""); + + int hardwareChannelCount = actualConfiguration.getHardwareChannelCount(); + int hardwareSampleRate = actualConfiguration.getHardwareSampleRate(); + int hardwareFormat = actualConfiguration.getHardwareFormat(); + msg += "\nHW: #ch=" + (hardwareChannelCount == + StreamConfiguration.UNSPECIFIED ? "?" : hardwareChannelCount); + msg += ", SR=" + (hardwareSampleRate == + StreamConfiguration.UNSPECIFIED ? "?" : hardwareSampleRate); + msg += ", format=" + (hardwareFormat == StreamConfiguration.UNSPECIFIED ? + "?" : StreamConfiguration.convertFormatToText(hardwareFormat)); + + mStreamInfoView.setText(msg); + + mHideableView.requestLayout(); + } + + // This must be called on the UI thread. + public void setStatusText(String msg) { + mStreamStatusView.setText(msg); + } + + public void setExclusiveMode(boolean b) { + mRequestedExclusiveView.setChecked(b); + } + + public void setFormat(int format) { + mFormatSpinner.setSelection(format); // position matches format + } + + public void setFormatConversionAllowed(boolean allowed) { + mFormatConversionBox.setChecked(allowed); + } + + private void onChannelCountSpinnerSelected() { + if (mChannelCountSpinner.getSelectedItemPosition() != 0) { + mChannelMaskSpinner.setSelection(0); // Override the previous channel mask selection + mIsChannelMaskLastSelected = false; + } + } + + private void onChannelMaskSpinnerSelected() { + if (mChannelMaskSpinner.getSelectedItemPosition() != 0) { + mChannelCountSpinner.setSelection(0); // Override the previous channel count selection + mIsChannelMaskLastSelected = true; + } + } + + private void onRequestAudioEffectClicked(boolean isChecked) { + if (isChecked){ + if (misOutput) { + mOutputEffectsLayout.setVisibility(VISIBLE); + } else { + mInputEffectsLayout.setVisibility(VISIBLE); + } + } else { + if (misOutput) { + mOutputEffectsLayout.setVisibility(GONE); + } else { + mInputEffectsLayout.setVisibility(GONE); + } + } + } + + public void setupEffects(int sessionId) { + if (misOutput) { + mBassBoost = new BassBoost(0, sessionId); + mBassBoost.setStrength((short) mBassBoostSeekBar.getProgress()); + mLoudnessEnhancer = new LoudnessEnhancer(sessionId); + mLoudnessEnhancer.setTargetGain((short) mLoudnessEnhancerSeekBar.getProgress()); + } else { + // If AEC is not available, the checkbox will be disabled in initializeViews(). + if (mAcousticEchoCancelerCheckBox.isEnabled()) { + mAcousticEchoCanceler = AcousticEchoCanceler.create(sessionId); + if (mAcousticEchoCanceler != null) { + boolean wasOn = mAcousticEchoCanceler.getEnabled(); + String text = mAcousticEchoCancelerText + "(" + (wasOn ? "Y" : "N") + ")"; + mAcousticEchoCancelerCheckBox.setText(text); + mAcousticEchoCanceler.setEnabled(mAcousticEchoCancelerCheckBox.isChecked()); + } else { + Log.e(TAG, String.format(Locale.getDefault(), "Could not create AcousticEchoCanceler")); + } + } + + // If AGC is not available, the checkbox will be disabled in initializeViews(). + if (mAutomaticGainControlCheckBox.isEnabled()) { + mAutomaticGainControl = AutomaticGainControl.create(sessionId); + if (mAutomaticGainControl != null) { + boolean wasOn = mAutomaticGainControl.getEnabled(); + String text = mAutomaticGainControlText + "(" + (wasOn ? "Y" : "N") + ")"; + mAutomaticGainControlCheckBox.setText(text); + mAutomaticGainControl.setEnabled(mAutomaticGainControlCheckBox.isChecked()); + } else { + Log.e(TAG, String.format(Locale.getDefault(), "Could not create AutomaticGainControl")); + } + } + + // If Noise Suppressor is not available, the checkbox will be disabled in initializeViews(). + if (mNoiseSuppressorCheckBox.isEnabled()) { + mNoiseSuppressor = NoiseSuppressor.create(sessionId); + if (mNoiseSuppressor != null) { + boolean wasOn = mNoiseSuppressor.getEnabled(); + String text = mNoiseSuppressorText + "(" + (wasOn ? "Y" : "N") + ")"; + mNoiseSuppressorCheckBox.setText(text); + mNoiseSuppressor.setEnabled(mNoiseSuppressorCheckBox.isChecked()); + } else { + Log.e(TAG, String.format(Locale.getDefault(), "Could not create NoiseSuppressor")); + } + } + } + } + + private void onLoudnessEnhancerSeekBarChanged(int progress) { + if (mLoudnessEnhancer != null) { + mLoudnessEnhancer.setTargetGain(progress); + } + mLoudnessEnhancerTextView.setText("Loudness Enhancer: " + progress); + } + + private void onBassBoostSeekBarChanged(int progress) { + if (mBassBoost != null) { + mBassBoost.setStrength((short) progress); + } + mBassBoostTextView.setText("Bass Boost: " + progress); + } + + private void onAutomaticGainControlCheckBoxChanged(boolean isChecked) { + if (mAutomaticGainControlCheckBox.isEnabled() && mAutomaticGainControl != null) { + mAutomaticGainControl.setEnabled(isChecked); + } + } + + private void onAcousticEchoCancelerCheckBoxChanged(boolean isChecked) { + if (mAcousticEchoCancelerCheckBox.isEnabled() && mAcousticEchoCanceler != null) { + mAcousticEchoCanceler.setEnabled(isChecked); + } + } + + private void onNoiseSuppressorCheckBoxChanged(boolean isChecked) { + if (mNoiseSuppressorCheckBox.isEnabled() && mNoiseSuppressor != null) { + mNoiseSuppressor.setEnabled(isChecked); + } + } +} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TapLatencyAnalyser.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TapLatencyAnalyser.java similarity index 72% rename from mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TapLatencyAnalyser.java rename to mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TapLatencyAnalyser.java index 9f902769..6b9d4b2a 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TapLatencyAnalyser.java +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TapLatencyAnalyser.java @@ -13,17 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.sample.oboe.manualtest; +package com.mobileer.oboetester; import java.util.ArrayList; +/** + * Analyze a recording and extract edges for latency analysis. + */ public class TapLatencyAnalyser { public static final int TYPE_TAP = 0; float[] mHighPassBuffer; private float mDroop = 0.995f; - private static float LOW_THRESHOLD = 0.01f; - private static float HIGH_THRESHOLD = 0.03f; + private static final float EDGE_THRESHOLD = 0.01f; + private static final float LOW_FRACTION = 0.5f; public static class TapLatencyEvent { public int type; @@ -38,8 +41,10 @@ public class TapLatencyAnalyser { // Use high pass filter to remove rumble from air conditioners. mHighPassBuffer = new float[numSamples]; highPassFilter(buffer, offset, numSamples, mHighPassBuffer); + // Apply envelope follower. float[] peakBuffer = new float[numSamples]; fillPeakBuffer(mHighPassBuffer, 0, numSamples, peakBuffer); + // Look for two attacks. return scanForEdges(peakBuffer, numSamples); } @@ -47,13 +52,14 @@ public class TapLatencyAnalyser { return mHighPassBuffer; } + // Based on https://en.wikipedia.org/wiki/High-pass_filter private void highPassFilter(float[] buffer, int offset, int numSamples, float[] highPassBuffer) { float xn1 = 0.0f; float yn1 = 0.0f; - float alpha = 0.05f; + final float alpha = 0.8f; for (int i = 0; i < numSamples; i++) { float xn = buffer[i + offset]; - float yn = alpha * yn1 + ((1.0f - alpha) * (xn - xn1)); + float yn = alpha * (yn1 + xn - xn1); highPassBuffer[i] = yn; xn1 = xn; yn1 = yn; @@ -64,45 +70,49 @@ public class TapLatencyAnalyser { ArrayList events = new ArrayList(); float slow = 0.0f; float fast = 0.0f; - float slowCoefficient = 0.01f; - float fastCoefficient = 0.10f; + final float slowCoefficient = 0.01f; + final float fastCoefficient = 0.10f; + float lowThreshold = EDGE_THRESHOLD; boolean armed = true; int sampleIndex = 0; for (float level : peakBuffer) { slow = slow + (level - slow) * slowCoefficient; // low pass filter - fast = fast + (level - fast) * fastCoefficient; - if (armed && (fast > HIGH_THRESHOLD) && (fast > (2.0 * slow))) { + fast = fast + (level - fast) * fastCoefficient; // low pass filter + if (armed && (fast > EDGE_THRESHOLD) && (fast > (2.0 * slow))) { //System.out.println("edge at " + sampleIndex + ", slow " + slow + ", fast " + fast); events.add(new TapLatencyEvent(TYPE_TAP, sampleIndex)); armed = false; + lowThreshold = fast * LOW_FRACTION; } // Use hysteresis when rearming. - if (!armed && (fast < LOW_THRESHOLD)) { + if (!armed && (fast < lowThreshold)) { armed = true; + // slow = fast; // This seems unnecessary. + //events.add(new TapLatencyEvent(TYPE_TAP, sampleIndex)); } sampleIndex++; } return events.toArray(new TapLatencyEvent[0]); } + /** + * Envelope follower that rides along the peaks of the waveforms + * and then decays exponentially. + * + * @param buffer + * @param offset + * @param numSamples + */ private void fillPeakBuffer(float[] buffer, int offset, int numSamples, float[] peakBuffer) { float previous = 0.0f; - float maxInput = 0.0f; - float maxOutput = 0.0f; for (int i = 0; i < numSamples; i++) { - float input = buffer[i + offset]; - if (input > maxInput) { - maxInput = input; - } + float input = Math.abs(buffer[i + offset]); float output = previous * mDroop; if (input > output) { output = input; } previous = output; peakBuffer[i] = output; - if (output > maxOutput) { - maxOutput = output; - } } } } diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TapToToneActivity.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TapToToneActivity.java new file mode 100644 index 00000000..26c7dc3e --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TapToToneActivity.java @@ -0,0 +1,305 @@ +/* + * Copyright 2015 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. + */ + +package com.mobileer.oboetester; + +import static com.mobileer.oboetester.MidiTapTester.NoteListener; + +import android.content.pm.PackageManager; +import android.media.AudioDeviceInfo; +import android.media.AudioManager; +import android.media.midi.MidiDevice; +import android.media.midi.MidiDeviceInfo; +import android.media.midi.MidiInputPort; +import android.media.midi.MidiManager; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; +import android.view.WindowManager; +import android.widget.Button; +import android.widget.Toast; + +import com.mobileer.audio_device.AudioDeviceListEntry; +import com.mobileer.audio_device.AudioDeviceSpinner; +import com.mobileer.miditools.MidiOutputPortConnectionSelector; +import com.mobileer.miditools.MidiPortConnector; +import com.mobileer.miditools.MidiTools; + +import java.io.IOException; +import java.sql.Timestamp; + +public class TapToToneActivity extends TestOutputActivityBase { + // Names from obsolete version of Oboetester. + public static final String OLD_PRODUCT_NAME = "AudioLatencyTester"; + public static final String OLD_MANUFACTURER_NAME = "AndroidTest"; + + private MidiManager mMidiManager; + private MidiInputPort mInputPort; + + protected MidiTapTester mMidiTapTester; + protected TapToToneTester mTapToToneTester; + + private Button mStopButton; + private Button mStartButton; + + private MidiOutputPortConnectionSelector mPortSelector; + private final MyNoteListener mTestListener = new MyNoteListener(); + + private AudioDeviceSpinner mInputDeviceSpinner; + + @Override + protected void inflateActivity() { + setContentView(R.layout.activity_tap_to_tone); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mAudioOutTester = addAudioOutputTester(); + + mTapToToneTester = new TapToToneTester(this, + getResources().getString(R.string.tap_to_tone_instructions)); + + if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_MIDI)) { + setupMidi(); + } else { + Toast.makeText(TapToToneActivity.this, + "MIDI not supported!", Toast.LENGTH_LONG) + .show(); + } + + + // Start a blip test when the waveform view is tapped. + WaveformView mWaveformView = (WaveformView) findViewById(R.id.waveview_audio); + mWaveformView.setOnTouchListener((view, event) -> { + // Do not call view.performClick() because it may trigger a touch sound! + int action = event.getActionMasked(); + switch (action) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_POINTER_DOWN: + trigger(); + break; + case MotionEvent.ACTION_MOVE: + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_POINTER_UP: + break; + } + // Must return true or we do not get the ACTION_MOVE and + // ACTION_UP events. + return true; + }); + + mCommunicationDeviceView = (CommunicationDeviceView) findViewById(R.id.comm_device_view); + + mStartButton = (Button) findViewById(R.id.button_start); + mStopButton = (Button) findViewById(R.id.button_stop); + updateButtons(false); + + updateEnabledWidgets(); + + mInputDeviceSpinner = (AudioDeviceSpinner) findViewById(R.id.input_devices_spinner); + mInputDeviceSpinner.setDirectionType(AudioManager.GET_DEVICES_INPUTS); + } + + private void updateButtons(boolean running) { + mStartButton.setEnabled(!running); + mStopButton.setEnabled(running); + } + + void trigger() { + if (mTapToToneTester.isArmed()) { + mAudioOutTester.trigger(); + mTapToToneTester.analyzeLater(getString(R.string.please_wait)); + Timestamp timestamp = new Timestamp(System.currentTimeMillis()); + Log.d(TAG, "Tap to Tone Triggered. Timestamp: " + timestamp); + } else { + showToast(getString(R.string.no_double_tap)); + } + } + + @Override + int getActivityType() { + return ACTIVITY_TAP_TO_TONE; + } + + @Override + protected void onDestroy() { + mMidiTapTester.removeTestListener(mTestListener); + closeMidiResources(); + super.onDestroy(); + } + + private void setupMidi() { + // Setup MIDI + mMidiManager = (MidiManager) getSystemService(MIDI_SERVICE); + MidiDeviceInfo[] infos = mMidiManager.getDevices(); + + // Warn if old version of OboeTester found. + for (MidiDeviceInfo info : infos) { + Log.i(TAG, "MIDI info = " + info); + Bundle properties = info.getProperties(); + String product = properties + .getString(MidiDeviceInfo.PROPERTY_PRODUCT); + String manufacturer = properties + .getString(MidiDeviceInfo.PROPERTY_MANUFACTURER); + if (OLD_PRODUCT_NAME.equals(product) && OLD_MANUFACTURER_NAME.equals(manufacturer)) { + showErrorToast("Please uninstall old version of OboeTester."); + break; + } + } + + // Open the port now so that the MidiTapTester gets created. + for (MidiDeviceInfo info : infos) { + Bundle properties = info.getProperties(); + String product = properties + .getString(MidiDeviceInfo.PROPERTY_PRODUCT); + if (MidiTapTester.PRODUCT_NAME.equals(product)) { + String manufacturer = properties + .getString(MidiDeviceInfo.PROPERTY_MANUFACTURER); + if (MidiTapTester.MANUFACTURER_NAME.equals(manufacturer)) { + openPortTemporarily(info); + break; + } + } + } + } + + // These should only be set after mAudioMidiTester is set. + private void setSpinnerListeners() { + MidiDeviceInfo synthInfo = MidiTools.findDevice(mMidiManager, MidiTapTester.MANUFACTURER_NAME, + MidiTapTester.PRODUCT_NAME); + Log.i(TAG, "found tester virtual device info: " + synthInfo); + int portIndex = 0; + mPortSelector = new MidiOutputPortConnectionSelector(mMidiManager, this, + R.id.spinner_synth_sender, synthInfo, portIndex); + mPortSelector.setConnectedListener(new MyPortsConnectedListener()); + + } + + private class MyNoteListener implements NoteListener { + @Override + public void onNoteOn(final int pitch) { + runOnUiThread(() -> { + trigger(); + mStreamContexts.get(0).configurationView.setStatusText("MIDI pitch = " + pitch); + }); + } + } + + private void openPortTemporarily(final MidiDeviceInfo info) { + Log.i(TAG, "MIDI openPort() info = " + info); + mMidiManager.openDevice(info, device -> { + if (device == null) { + Log.e(TAG, "could not open device " + info); + } else { + mInputPort = device.openInputPort(0); + Log.i(TAG, "opened MIDI port = " + mInputPort + " on " + info); + mMidiTapTester = MidiTapTester.getInstanceOrNull(); + if (mMidiTapTester == null) { + Log.e(TAG, "MidiTapTester Service was not created! info = " + info); + showErrorToast("MidiTapTester Service was not created!"); + } else { + Log.i(TAG, "openPort() mMidiTapTester = " + mMidiTapTester); + // Now that we have created the MidiTapTester, close the port so we can + // open it later. + try { + mInputPort.close(); + } catch (IOException e) { + e.printStackTrace(); + } + mMidiTapTester.addTestListener(mTestListener); + setSpinnerListeners(); + } + } + }, new Handler(Looper.getMainLooper()) + ); + } + + // TODO Listen to the synth server + // for open/close events and then disable/enable the spinner. + private class MyPortsConnectedListener + implements MidiPortConnector.OnPortsConnectedListener { + @Override + public void onPortsConnected(final MidiDevice.MidiConnection connection) { + Log.i(TAG, "onPortsConnected, connection = " + connection); + runOnUiThread(() -> { + if (connection == null) { + Toast.makeText(TapToToneActivity.this, + R.string.error_port_busy, Toast.LENGTH_LONG) + .show(); + mPortSelector.clearSelection(); + } else { + Toast.makeText(TapToToneActivity.this, + R.string.port_open_ok, Toast.LENGTH_LONG) + .show(); + } + }); + } + } + + private void closeMidiResources() { + if (mPortSelector != null) { + mPortSelector.close(); + } + } + + public void startTest(View view) { + try { + openAudio(); + } catch (IOException e) { + e.printStackTrace(); + showErrorToast("Open audio failed!"); + return; + } + try { + super.startAudio(); + startTapToToneTester(); + updateButtons(true); + } catch (IOException e) { + e.printStackTrace(); + showErrorToast("Start audio failed! " + e.getMessage()); + return; + } + } + + public void stopTest(View view) { + stopTapToToneTester(); + stopAudio(); + closeAudio(); + updateButtons(false); + } + + private void startTapToToneTester() throws IOException { + AudioDeviceInfo deviceInfo = + ((AudioDeviceListEntry) mInputDeviceSpinner.getSelectedItem()).getDeviceInfo(); + mTapToToneTester.setInputDevice(deviceInfo); + mInputDeviceSpinner.setEnabled(false); + mTapToToneTester.resetLatency(); + mTapToToneTester.start(); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + + private void stopTapToToneTester() { + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + mInputDeviceSpinner.setEnabled(true); + mTapToToneTester.stop(); + } +} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TapToToneTester.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TapToToneTester.java new file mode 100644 index 00000000..ce92a25a --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TapToToneTester.java @@ -0,0 +1,207 @@ +package com.mobileer.oboetester; + +import android.app.Activity; +import android.media.AudioDeviceInfo; +import android.widget.TextView; + +import java.io.IOException; +import java.util.Locale; + +/** + * Measure tap-to-tone latency by and update the waveform display. + */ +public class TapToToneTester { + + private static final float MAX_TOUCH_LATENCY = 0.200f; + private static final float MAX_OUTPUT_LATENCY = 1.200f; + private static final float ANALYSIS_TIME_MARGIN = 0.500f; + + private static final float ANALYSIS_TIME_DELAY = MAX_OUTPUT_LATENCY; + private static final float ANALYSIS_TIME_TOTAL = MAX_TOUCH_LATENCY + MAX_OUTPUT_LATENCY; + private static final int ANALYSIS_SAMPLE_RATE = 48000; // need not match output rate + + private final boolean mRecordEnabled = true; + private final AudioRecordThread mRecorder; + private final TapLatencyAnalyser mTapLatencyAnalyser; + + private final Activity mActivity; + private final WaveformView mWaveformView; + private final TextView mResultView; + + private final String mTapInstructions; + private float mAnalysisTimeMargin = ANALYSIS_TIME_MARGIN; + + private boolean mArmed = true; + + // Stats for latency + private int mMeasurementCount; + private int mLatencySumSamples; + private int mLatencyMin; + private int mLatencyMax; + + public static class TestResult { + public float[] samples; + public float[] filtered; + public int frameRate; + public TapLatencyAnalyser.TapLatencyEvent[] events; + } + + public TapToToneTester(Activity activity, String tapInstructions) { + mActivity = activity; + mTapInstructions = tapInstructions; + mResultView = (TextView) activity.findViewById(R.id.resultView); + mWaveformView = (WaveformView) activity.findViewById(R.id.waveview_audio); + mWaveformView.setEnabled(false); + + if (mRecordEnabled) { + float analysisTimeMax = ANALYSIS_TIME_TOTAL + mAnalysisTimeMargin; + mRecorder = new AudioRecordThread(ANALYSIS_SAMPLE_RATE, + 1, + (int) (analysisTimeMax * ANALYSIS_SAMPLE_RATE)); + } + mTapLatencyAnalyser = new TapLatencyAnalyser(); + } + + public void start() throws IOException { + if (mRecordEnabled) { + mRecorder.startAudio(); + mWaveformView.setEnabled(true); + } + } + + public void stop() { + if (mRecordEnabled) { + mRecorder.stopAudio(); + mWaveformView.setEnabled(false); + } + } + + /** + * @return true if ready to process a tap, false if already triggered + */ + public boolean isArmed() { + return mArmed; + } + + public void setArmed(boolean armed) { + this.mArmed = armed; + } + + public void analyzeLater(String message) { + showPendingStatus(message); + Runnable task = this::analyseAndShowResults; + scheduleTaskWhenDone(task); + mArmed = false; + } + + private void showPendingStatus(final String message) { + mWaveformView.post(() -> { + mWaveformView.setMessage(message); + mWaveformView.clearSampleData(); + mWaveformView.invalidate(); + }); + } + + private void scheduleTaskWhenDone(Runnable task) { + if (mRecordEnabled) { + // schedule an analysis to start in the near future + int numSamples = (int) (mRecorder.getSampleRate() * ANALYSIS_TIME_DELAY); + mRecorder.scheduleTask(numSamples, task); + } + } + + private void analyseAndShowResults() { + TestResult result = analyzeCapturedAudio(); + if (result != null) { + showTestResults(result); + } + } + + public TestResult analyzeCapturedAudio() { + if (!mRecordEnabled) return null; + int numSamples = (int) (mRecorder.getSampleRate() * ANALYSIS_TIME_TOTAL); + float[] buffer = new float[numSamples]; + mRecorder.setCaptureEnabled(false); // TODO wait for it to settle + int numRead = mRecorder.readMostRecent(buffer); + + TestResult result = new TestResult(); + result.samples = buffer; + result.frameRate = mRecorder.getSampleRate(); + result.events = mTapLatencyAnalyser.analyze(buffer, 0, numRead); + result.filtered = mTapLatencyAnalyser.getFilteredBuffer(); + mRecorder.setCaptureEnabled(true); + return result; + } + + public void resetLatency() { + mMeasurementCount = 0; + mLatencySumSamples = 0; + mLatencyMin = Integer.MAX_VALUE; + mLatencyMax = 0; + showTestResults(null); + } + + // Runs on UI thread. + public void showTestResults(TestResult result) { + String text; + mWaveformView.setMessage(null); + if (result == null) { + text = mTapInstructions; + mWaveformView.clearSampleData(); + } else { + // Show edges detected. + if (result.events.length == 0) { + mWaveformView.setCursorData(null); + } else { + int numEdges = Math.min(8, result.events.length); + int[] cursors = new int[numEdges]; + for (int i = 0; i < numEdges; i++) { + cursors[i] = result.events[i].sampleIndex; + } + mWaveformView.setCursorData(cursors); + } + // Did we get a good measurement? + if (result.events.length < 2) { + text = "Not enough edges. Use fingernail.\n"; + } else if (result.events.length > 2) { + text = "Too many edges.\n"; + } else { + int latencySamples = result.events[1].sampleIndex - result.events[0].sampleIndex; + mLatencySumSamples += latencySamples; + mMeasurementCount++; + + int latencyMillis = 1000 * latencySamples / result.frameRate; + if (mLatencyMin > latencyMillis) { + mLatencyMin = latencyMillis; + } + if (mLatencyMax < latencyMillis) { + mLatencyMax = latencyMillis; + } + + text = String.format(Locale.getDefault(), "tap-to-tone latency = %3d msec\n", latencyMillis); + } + mWaveformView.setSampleData(result.filtered); + } + + if (mMeasurementCount > 0) { + int averageLatencySamples = mLatencySumSamples / mMeasurementCount; + int averageLatencyMillis = 1000 * averageLatencySamples / result.frameRate; + final String plural = (mMeasurementCount == 1) ? "test" : "tests"; + text = text + String.format(Locale.getDefault(), "min = %3d, avg = %3d, max = %3d, %d %s", + mLatencyMin, averageLatencyMillis, mLatencyMax, mMeasurementCount, plural); + } + final String postText = text; + mWaveformView.post(new Runnable() { + public void run() { + mResultView.setText(postText); + mWaveformView.postInvalidate(); + } + }); + + mArmed = true; + } + + void setInputDevice(AudioDeviceInfo deviceInfo) { + mRecorder.setInputDevice(deviceInfo); + } +} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestAudioActivity.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestAudioActivity.java new file mode 100644 index 00000000..f286ffbb --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestAudioActivity.java @@ -0,0 +1,919 @@ +/* + * Copyright 2017 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. + */ + +package com.mobileer.oboetester; + +import static com.mobileer.oboetester.AudioForegroundService.ACTION_START; +import static com.mobileer.oboetester.AudioForegroundService.ACTION_STOP; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.ServiceInfo; +import android.media.AudioAttributes; +import android.media.AudioDeviceInfo; +import android.media.AudioManager; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import android.view.View; +import android.view.WindowManager; +import android.widget.AdapterView; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.Spinner; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Locale; + +/** + * Base class for other Activities. + */ +abstract class TestAudioActivity extends AppCompatActivity { + public static final String TAG = "OboeTester"; + + protected static final int FADER_PROGRESS_MAX = 1000; + private static final int INTENT_TEST_DELAY_MILLIS = 1100; + + public static final int AUDIO_STATE_OPEN = 0; + public static final int AUDIO_STATE_STARTED = 1; + public static final int AUDIO_STATE_PAUSED = 2; + public static final int AUDIO_STATE_FLUSHED = 3; + public static final int AUDIO_STATE_STOPPED = 4; + public static final int AUDIO_STATE_RELEASED = 5; + public static final int AUDIO_STATE_CLOSING = 6; + public static final int AUDIO_STATE_CLOSED = 7; + + public static final int COLOR_ACTIVE = 0xFFD0D0A0; + public static final int COLOR_IDLE = 0xFFD0D0D0; + + // Pass the activity index to native so it can know how to respond to the start and stop calls. + // WARNING - must match definitions in NativeAudioContext.h ActivityType + public static final int ACTIVITY_TEST_OUTPUT = 0; + public static final int ACTIVITY_TEST_INPUT = 1; + public static final int ACTIVITY_TAP_TO_TONE = 2; + public static final int ACTIVITY_RECORD_PLAY = 3; + public static final int ACTIVITY_ECHO = 4; + public static final int ACTIVITY_RT_LATENCY = 5; + public static final int ACTIVITY_GLITCHES = 6; + public static final int ACTIVITY_TEST_DISCONNECT = 7; + public static final int ACTIVITY_DATA_PATHS = 8; + public static final int ACTIVITY_DYNAMIC_WORKLOAD = 9; + + private int mAudioState = AUDIO_STATE_CLOSED; + + protected ArrayList mStreamContexts; + private Button mOpenButton; + private Button mStartButton; + private Button mPauseButton; + private Button mFlushButton; + private Button mStopButton; + private Button mReleaseButton; + private Button mCloseButton; + private MyStreamSniffer mStreamSniffer; + private CheckBox mCallbackReturnStopBox; + private Spinner mHangTimeSpinner; + + // Only set in some activities + protected CommunicationDeviceView mCommunicationDeviceView; + private int mSampleRate; + private int mSingleTestIndex = -1; + private static boolean mBackgroundEnabled; + private static boolean mForegroundServiceEnabled; + + protected Bundle mBundleFromIntent; + protected boolean mTestRunningByIntent; + protected String mResultFileName; + private String mTestResults; + private ExternalFileWriter mExternalFileWriter = new ExternalFileWriter(this); + + public String getTestName() { + return "TestAudio"; + } + + public static class StreamContext { + StreamConfigurationView configurationView; + AudioStreamTester tester; + + boolean isInput() { + return tester.getCurrentAudioStream().isInput(); + } + } + + // Periodically query the status of the streams. + protected class MyStreamSniffer { + public static final int SNIFFER_UPDATE_PERIOD_MSEC = 150; + public static final int SNIFFER_UPDATE_DELAY_MSEC = 300; + + private Handler mHandler; + + // Display status info for the stream. + private Runnable runnableCode = new Runnable() { + @Override + public void run() { + boolean streamClosed = false; + boolean gotViews = false; + for (StreamContext streamContext : mStreamContexts) { + AudioStreamBase.StreamStatus status = streamContext.tester.getCurrentAudioStream().getStreamStatus(); + AudioStreamBase.DoubleStatistics latencyStatistics = + streamContext.tester.getCurrentAudioStream().getLatencyStatistics(); + if (streamContext.configurationView != null) { + // Handler runs this on the main UI thread. + int framesPerBurst = streamContext.tester.getCurrentAudioStream().getFramesPerBurst(); + status.framesPerCallback = getFramesPerCallback(); + String msg = ""; + msg += "timestamp.latency = " + latencyStatistics.dump() + "\n"; + msg += status.dump(framesPerBurst); + streamContext.configurationView.setStatusText(msg); + updateStreamDisplay(); + gotViews = true; + } + + streamClosed = streamClosed || (status.state >= 12); + } + + if (streamClosed) { + onStreamClosed(); + } else { + // Repeat this runnable code block again. + if (gotViews) { + mHandler.postDelayed(runnableCode, SNIFFER_UPDATE_PERIOD_MSEC); + } + } + } + }; + + private void startStreamSniffer() { + stopStreamSniffer(); + mHandler = new Handler(Looper.getMainLooper()); + // Start the initial runnable task by posting through the handler + mHandler.postDelayed(runnableCode, SNIFFER_UPDATE_DELAY_MSEC); + } + + private void stopStreamSniffer() { + if (mHandler != null) { + mHandler.removeCallbacks(runnableCode); + } + } + + } + + public static void setBackgroundEnabled(boolean enabled) { + mBackgroundEnabled = enabled; + } + + public static boolean isBackgroundEnabled() { + return mBackgroundEnabled; + } + + public static void setForegroundServiceEnabled(boolean enabled) { + mForegroundServiceEnabled = enabled; + } + + public static boolean isForegroundServiceEnabled() { + return mForegroundServiceEnabled; + } + + public int getServiceType() { + switch(getActivityType()) { + case ACTIVITY_TEST_OUTPUT: + return ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK; + case ACTIVITY_TEST_INPUT: + return ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE; + case ACTIVITY_TAP_TO_TONE: + return ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK + | ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE; + case ACTIVITY_RECORD_PLAY: + return ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK + | ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE; + case ACTIVITY_ECHO: + return ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK + | ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE; + case ACTIVITY_RT_LATENCY: + return ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK + | ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE; + case ACTIVITY_GLITCHES: + return ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK + | ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE; + case ACTIVITY_TEST_DISCONNECT: + return ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK + | ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE; + case ACTIVITY_DATA_PATHS: + return ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK + | ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE; + case ACTIVITY_DYNAMIC_WORKLOAD: + return ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK; + default: + Log.i(TAG, "getServiceType() called on unknown activity type " + getActivityType()); + return 0; + } + } + + public void onStreamClosed() { + } + + protected abstract void inflateActivity(); + + void updateStreamDisplay() { + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + inflateActivity(); + findAudioCommon(); + + mBundleFromIntent = getIntent().getExtras(); + } + + @Override + public void onNewIntent(Intent intent) { + super.onNewIntent(intent); + mBundleFromIntent = intent.getExtras(); + } + + public boolean isTestConfiguredUsingBundle() { + return mBundleFromIntent != null; + } + + public void hideSettingsViews() { + for (StreamContext streamContext : mStreamContexts) { + if (streamContext.configurationView != null) { + streamContext.configurationView.hideSettingsView(); + } + } + } + + abstract int getActivityType(); + + public void setSingleTestIndex(int testIndex) { + mSingleTestIndex = testIndex; + } + + public int getSingleTestIndex() { + return mSingleTestIndex; + } + + @Override + protected void onStart() { + super.onStart(); + resetConfiguration(); + setActivityType(getActivityType()); + // TODO Use LifeCycleObserver instead of this. + if (mCommunicationDeviceView != null) { + mCommunicationDeviceView.onStart(); + } + if (isForegroundServiceEnabled()) { + enableForegroundService(true); + } + } + + protected void resetConfiguration() { + } + + @Override + public void onResume() { + super.onResume(); + if (mBundleFromIntent != null) { + processBundleFromIntent(); + } + } + + private void setVolumeFromIntent() { + float normalizedVolume = IntentBasedTestSupport.getNormalizedVolumeFromBundle(mBundleFromIntent); + if (normalizedVolume >= 0.0) { + int streamType = IntentBasedTestSupport.getVolumeStreamTypeFromBundle(mBundleFromIntent); + AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + int maxVolume = audioManager.getStreamMaxVolume(streamType); + int requestedVolume = (int) (maxVolume * normalizedVolume); + audioManager.setStreamVolume(streamType, requestedVolume, 0); + } + } + + private void processBundleFromIntent() { + if (mTestRunningByIntent) { + return; + } + + // Delay the test start to avoid race conditions. See Oboe Issue #1533 + mTestRunningByIntent = true; + Handler handler = new Handler(Looper.getMainLooper()); // UI thread + handler.postDelayed(new DelayedTestByIntentRunnable(), + INTENT_TEST_DELAY_MILLIS); // Delay long enough to get past the onStop() call! + + } + + private class DelayedTestByIntentRunnable implements Runnable { + @Override + public void run() { + try { + mResultFileName = mBundleFromIntent.getString(IntentBasedTestSupport.KEY_FILE_NAME); + setVolumeFromIntent(); + startTestUsingBundle(); + } catch( Exception e) { + showErrorToast(e.getMessage()); + } + } + } + + public void startTestUsingBundle() { + } + + @Override + protected void onPause() { + super.onPause(); + } + + @Override + protected void onStop() { + if (!isBackgroundEnabled()) { + Log.i(TAG, "onStop() called so stop the test ========================="); + onStopTest(); + if (isForegroundServiceEnabled()) { + enableForegroundService(false); + } + } + if (mCommunicationDeviceView != null) { + mCommunicationDeviceView.onStop(); + } + super.onStop(); + } + + @Override + protected void onDestroy() { + if (isBackgroundEnabled()) { + Log.i(TAG, "onDestroy() called so stop the test ========================="); + onStopTest(); + if (isForegroundServiceEnabled()) { + enableForegroundService(false); + } + } + mAudioState = AUDIO_STATE_CLOSED; + super.onDestroy(); + } + + public void enableForegroundService(boolean enabled) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + String action = enabled ? ACTION_START : ACTION_STOP; + Intent serviceIntent = new Intent(action, null, this, + AudioForegroundService.class); + serviceIntent.putExtra("service_types", getServiceType()); + startForegroundService(serviceIntent); + } + } + + protected void updateEnabledWidgets() { + if (mOpenButton != null) { + mOpenButton.setBackgroundColor(mAudioState == AUDIO_STATE_OPEN ? COLOR_ACTIVE : COLOR_IDLE); + mStartButton.setBackgroundColor(mAudioState == AUDIO_STATE_STARTED ? COLOR_ACTIVE : COLOR_IDLE); + mPauseButton.setBackgroundColor(mAudioState == AUDIO_STATE_PAUSED ? COLOR_ACTIVE : COLOR_IDLE); + mFlushButton.setBackgroundColor(mAudioState == AUDIO_STATE_FLUSHED ? COLOR_ACTIVE : COLOR_IDLE); + mStopButton.setBackgroundColor(mAudioState == AUDIO_STATE_STOPPED ? COLOR_ACTIVE : COLOR_IDLE); + mReleaseButton.setBackgroundColor(mAudioState == AUDIO_STATE_RELEASED ? COLOR_ACTIVE : COLOR_IDLE); + mCloseButton.setBackgroundColor(mAudioState == AUDIO_STATE_CLOSED ? COLOR_ACTIVE : COLOR_IDLE); + } + setConfigViewsEnabled(mAudioState == AUDIO_STATE_CLOSED); + } + + private void setConfigViewsEnabled(boolean b) { + for (StreamContext streamContext : mStreamContexts) { + if (streamContext.configurationView != null) { + streamContext.configurationView.setChildrenEnabled(b); + } + } + } + + private void applyConfigurationViewsToModels() { + for (StreamContext streamContext : mStreamContexts) { + if (streamContext.configurationView != null) { + streamContext.configurationView.applyToModel(streamContext.tester.requestedConfiguration); + } + } + } + + abstract boolean isOutput(); + + public void clearStreamContexts() { + mStreamContexts.clear(); + } + + public StreamContext addOutputStreamContext() { + StreamContext streamContext = new StreamContext(); + streamContext.tester = AudioOutputTester.getInstance(); + streamContext.configurationView = (StreamConfigurationView) + findViewById(R.id.outputStreamConfiguration); + if (streamContext.configurationView == null) { + streamContext.configurationView = (StreamConfigurationView) + findViewById(R.id.streamConfiguration); + } + if (streamContext.configurationView != null) { + streamContext.configurationView.setOutput(true); + } + mStreamContexts.add(streamContext); + return streamContext; + } + + public AudioOutputTester addAudioOutputTester() { + StreamContext streamContext = addOutputStreamContext(); + return (AudioOutputTester) streamContext.tester; + } + + public StreamContext addInputStreamContext() { + StreamContext streamContext = new StreamContext(); + streamContext.tester = AudioInputTester.getInstance(); + streamContext.configurationView = (StreamConfigurationView) + findViewById(R.id.inputStreamConfiguration); + if (streamContext.configurationView == null) { + streamContext.configurationView = (StreamConfigurationView) + findViewById(R.id.streamConfiguration); + } + if (streamContext.configurationView != null) { + streamContext.configurationView.setOutput(false); + } + mStreamContexts.add(streamContext); + return streamContext; + } + + public AudioInputTester addAudioInputTester() { + StreamContext streamContext = addInputStreamContext(); + return (AudioInputTester) streamContext.tester; + } + + void updateStreamConfigurationViews() { + for (StreamContext streamContext : mStreamContexts) { + if (streamContext.configurationView != null) { + streamContext.configurationView.updateDisplay(streamContext.tester.actualConfiguration); + } + } + } + + StreamContext getFirstInputStreamContext() { + for (StreamContext streamContext : mStreamContexts) { + if (streamContext.isInput()) + return streamContext; + } + return null; + } + + StreamContext getFirstOutputStreamContext() { + for (StreamContext streamContext : mStreamContexts) { + if (!streamContext.isInput()) + return streamContext; + } + return null; + } + + protected void findAudioCommon() { + mOpenButton = (Button) findViewById(R.id.button_open); + if (mOpenButton != null) { + mStartButton = (Button) findViewById(R.id.button_start); + mPauseButton = (Button) findViewById(R.id.button_pause); + mFlushButton = (Button) findViewById(R.id.button_flush); + mStopButton = (Button) findViewById(R.id.button_stop); + mReleaseButton = (Button) findViewById(R.id.button_release); + mCloseButton = (Button) findViewById(R.id.button_close); + } + mStreamContexts = new ArrayList(); + + mCallbackReturnStopBox = (CheckBox) findViewById(R.id.callbackReturnStop); + if (mCallbackReturnStopBox != null) { + mCallbackReturnStopBox.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + OboeAudioStream.setCallbackReturnStop(mCallbackReturnStopBox.isChecked()); + } + }); + } + OboeAudioStream.setCallbackReturnStop(false); + + mHangTimeSpinner = (Spinner) findViewById(R.id.spinner_hang_time); + if (mHangTimeSpinner != null) { + mHangTimeSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + public void onItemSelected(AdapterView parent, View view, int position, long id) { + String hangTimeText = (String) mHangTimeSpinner.getAdapter().getItem(position); + int hangTimeMillis = Integer.parseInt(hangTimeText); + Log.d(TAG, "Hang Time = " + hangTimeMillis + " msec"); + + OboeAudioStream.setHangTimeMillis(hangTimeMillis); + } + + public void onNothingSelected(AdapterView parent) { + OboeAudioStream.setHangTimeMillis(0); + } + }); + } + OboeAudioStream.setHangTimeMillis(0); + + mStreamSniffer = new MyStreamSniffer(); + } + + private void updateNativeAudioParameters() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + AudioManager myAudioMgr = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + String text = myAudioMgr.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE); + int audioManagerSampleRate = Integer.parseInt(text); + text = myAudioMgr.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER); + int audioManagerFramesPerBurst = Integer.parseInt(text); + setDefaultAudioValues(audioManagerSampleRate, audioManagerFramesPerBurst); + } + } + + protected void showErrorToast(String message) { + Log.e(TAG, "showErrorToast(\"" + message + "\")"); + String text = "Error: " + message; + showToast(text); + } + + protected void showToast(final String message) { + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(TestAudioActivity.this, + message, + Toast.LENGTH_SHORT).show(); + } + }); + } + + private void onStartAllContexts() { + for (StreamContext streamContext : mStreamContexts) { + streamContext.tester.getCurrentAudioStream().onStart(); + } + } + private void onStopAllContexts() { + for (StreamContext streamContext : mStreamContexts) { + streamContext.tester.getCurrentAudioStream().onStop(); + } + } + + public void openAudio(View view) { + try { + openAudio(); + } catch (Exception e) { + showErrorToast("openAudio() caught " + e.getMessage()); + } + } + + void clearHangTime() { + OboeAudioStream.setHangTimeMillis(0); + if (mHangTimeSpinner != null) { + mHangTimeSpinner.setSelection(0); + } + } + + public void startAudio(View view) { + Log.i(TAG, "startAudio() called ======================================="); + clearHangTime(); // start running + try { + startAudio(); + } catch (Exception e) { + showErrorToast("startAudio() caught " + e.getMessage()); + } + keepScreenOn(true); + } + + protected void keepScreenOn(boolean on) { + if (on) { + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } else { + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + } + + public void stopAudio(View view) { + stopAudio(); + keepScreenOn(false); + } + + public void pauseAudio(View view) { + pauseAudio(); + keepScreenOn(false); + } + + public void flushAudio(View view) { + flushAudio(); + } + + public void closeAudio(View view) { + closeAudio(); + } + + public void releaseAudio(View view) { + releaseAudio(); + } + + public int getSampleRate() { + return mSampleRate; + } + + public void openAudio() throws IOException { + closeAudio(); + + updateNativeAudioParameters(); + + if (!isTestConfiguredUsingBundle()) { + applyConfigurationViewsToModels(); + } + + int sampleRate = 0; // Use the OUTPUT sample rate for INPUT + + // Open output streams then open input streams. + // This is so that the capacity of input stream can be expanded to + // match the burst size of the output for full duplex. + for (StreamContext streamContext : mStreamContexts) { + if (!streamContext.isInput()) { // OUTPUT? + openStreamContext(streamContext); + int streamSampleRate = streamContext.tester.actualConfiguration.getSampleRate(); + if (sampleRate == 0) { + sampleRate = streamSampleRate; + } + + if (shouldSetStreamControlByAttributes()) { + // Associate volume keys with this output stream. + int actualUsage = streamContext.tester.actualConfiguration.getUsage(); + int actualContentType = streamContext.tester.actualConfiguration.getContentType(); + setStreamControlByAttributes(actualUsage, actualContentType); + } + } + } + for (StreamContext streamContext : mStreamContexts) { + if (streamContext.isInput()) { + if (sampleRate != 0) { + streamContext.tester.requestedConfiguration.setSampleRate(sampleRate); + } + openStreamContext(streamContext); + } + } + updateEnabledWidgets(); + onStartAllContexts(); + mStreamSniffer.startStreamSniffer(); + } + + protected boolean shouldSetStreamControlByAttributes() { + return true; + } + + /** + * Associate the volume keys with the stream we are playing. + * @param usage usage for the stream + * @param contentType tupe of the stream + */ + private void setStreamControlByAttributes(int usage, int contentType) { + AudioAttributes attributes = new AudioAttributes.Builder().setUsage(usage) + .setContentType(contentType).build(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + int volumeControlStream = attributes.getVolumeControlStream(); + Log.i(TAG, "setVolumeControlStream(" + volumeControlStream + ")"); + setVolumeControlStream(volumeControlStream); + } + } + + /** + * @param deviceId + * @return true if the device is TYPE_BLUETOOTH_SCO + */ + boolean isScoDevice(int deviceId) { + if (deviceId == 0) return false; // Unspecified + AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + final AudioDeviceInfo[] devices = audioManager.getDevices( + AudioManager.GET_DEVICES_INPUTS | AudioManager.GET_DEVICES_OUTPUTS); + for (AudioDeviceInfo device : devices) { + if (device.getId() == deviceId) { + return device.getType() == AudioDeviceInfo.TYPE_BLUETOOTH_SCO; + } + } + return false; + } + + private void openStreamContext(StreamContext streamContext) throws IOException { + StreamConfiguration requestedConfig = streamContext.tester.requestedConfiguration; + StreamConfiguration actualConfig = streamContext.tester.actualConfiguration; + + streamContext.tester.open(); // OPEN the stream + + mSampleRate = actualConfig.getSampleRate(); + mAudioState = AUDIO_STATE_OPEN; + int sessionId = actualConfig.getSessionId(); + if (streamContext.configurationView != null) { + if (sessionId > 0) { + try { + streamContext.configurationView.setupEffects(sessionId); + } catch (Exception e) { + showErrorToast("openStreamContext() caught " + e.getMessage()); + } + } + streamContext.configurationView.updateDisplay(streamContext.tester.actualConfiguration); + } + } + + // Native methods + private native int startNative(); + + private native int pauseNative(); + + private native int flushNative(); + + private native int stopNative(); + + private native int releaseNative(); + + protected native void setActivityType(int activityType); + + private native int getFramesPerCallback(); + + public native void setUseAlternativeAdpf(boolean enabled); + + private static native void setDefaultAudioValues(int audioManagerSampleRate, int audioManagerFramesPerBurst); + + public void startAudio() throws IOException { + Log.i(TAG, "startAudio() called ========================="); + int result = startNative(); + if (result != 0) { + showErrorToast("Start failed with " + result + ", " + StreamConfiguration.convertErrorToText(result)); + throw new IOException("startNative returned " + result + ", " + StreamConfiguration.convertErrorToText(result)); + } else { + onStartAllContexts(); + for (StreamContext streamContext : mStreamContexts) { + StreamConfigurationView configView = streamContext.configurationView; + if (configView != null) { + configView.updateDisplay(streamContext.tester.actualConfiguration); + } + } + mAudioState = AUDIO_STATE_STARTED; + updateEnabledWidgets(); + } + } + + protected void toastPauseError(int result) { + showErrorToast("Pause failed with " + result + ", " + StreamConfiguration.convertErrorToText(result)); + } + + public void pauseAudio() { + int result = pauseNative(); + if (result != 0) { + toastPauseError(result); + } else { + mAudioState = AUDIO_STATE_PAUSED; + updateEnabledWidgets(); + onStopAllContexts(); + } + } + + public void flushAudio() { + int result = flushNative(); + if (result != 0) { + showErrorToast("Flush failed with " + result + ", " + StreamConfiguration.convertErrorToText(result)); + } else { + mAudioState = AUDIO_STATE_FLUSHED; + updateEnabledWidgets(); + } + } + + public void stopAudio() { + int result = stopNative(); + if (result != 0) { + showErrorToast("Stop failed with " + result + ", " + StreamConfiguration.convertErrorToText(result)); + } else { + mAudioState = AUDIO_STATE_STOPPED; + updateEnabledWidgets(); + onStopAllContexts(); + } + } + + public void releaseAudio() { + int result = releaseNative(); + if (result != 0) { + showErrorToast("Release failed with " + result + ", " + StreamConfiguration.convertErrorToText(result)); + } else { + mAudioState = AUDIO_STATE_RELEASED; + updateEnabledWidgets(); + onStopAllContexts(); + } + } + + public void runTest() { + } + + public void saveIntentLog() { + } + + // This should only be called from UI events such as onStop or a button press. + public void onStopTest() { + stopTest(); + } + + public void stopTest() { + stopAudio(); + closeAudio(); + } + + public void stopAudioQuiet() { + stopNative(); + mAudioState = AUDIO_STATE_STOPPED; + updateEnabledWidgets(); + } + + // Make synchronized so we don't close from two streams at the same time. + public synchronized void closeAudio() { + if (mAudioState >= AUDIO_STATE_CLOSING) { + Log.d(TAG, "closeAudio() already closing"); + return; + } + mAudioState = AUDIO_STATE_CLOSING; + + mStreamSniffer.stopStreamSniffer(); + // Close output streams first because legacy callbacks may still be active + // and an output stream may be calling the input stream. + for (StreamContext streamContext : mStreamContexts) { + if (!streamContext.isInput()) { + streamContext.tester.close(); + } + } + for (StreamContext streamContext : mStreamContexts) { + if (streamContext.isInput()) { + streamContext.tester.close(); + } + } + + mAudioState = AUDIO_STATE_CLOSED; + updateEnabledWidgets(); + } + + void startBluetoothSco() { + AudioManager myAudioMgr = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + myAudioMgr.startBluetoothSco(); + } + + void stopBluetoothSco() { + AudioManager myAudioMgr = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + myAudioMgr.stopBluetoothSco(); + } + + @NonNull + protected String getCommonTestReport() { + StringBuffer report = new StringBuffer(); + // Add some extra information for the remote tester. + report.append("build.fingerprint = " + Build.FINGERPRINT + "\n"); + try { + PackageInfo pinfo = getPackageManager().getPackageInfo(getPackageName(), 0); + report.append(String.format(Locale.getDefault(), "test.version = %s\n", pinfo.versionName)); + report.append(String.format(Locale.getDefault(), "test.version.code = %d\n", pinfo.versionCode)); + } catch (PackageManager.NameNotFoundException e) { + } + report.append("time.millis = " + System.currentTimeMillis() + "\n"); + + if (mStreamContexts.size() == 0) { + report.append("ERROR: no active streams" + "\n"); + } else { + StreamContext streamContext = mStreamContexts.get(0); + AudioStreamTester streamTester = streamContext.tester; + report.append(streamTester.actualConfiguration.dump()); + AudioStreamBase.StreamStatus status = streamTester.getCurrentAudioStream().getStreamStatus(); + AudioStreamBase.DoubleStatistics latencyStatistics = + streamTester.getCurrentAudioStream().getLatencyStatistics(); + int framesPerBurst = streamTester.getCurrentAudioStream().getFramesPerBurst(); + status.framesPerCallback = getFramesPerCallback(); + report.append("timestamp.latency = " + latencyStatistics.dump() + "\n"); + // TODO The following report is not in a name=value format! + // report.append(status.dump(framesPerBurst)); + } + + return report.toString(); + } + + File maybeWriteTestResult(String resultString) { + File fileWritten = null; + if (mResultFileName != null) { + try { + fileWritten = mExternalFileWriter.writeStringToExternalFile(resultString, mResultFileName); + } catch (IOException e) { + e.printStackTrace(); + showErrorToast(" writing result file. " + e.getMessage()); + } + mResultFileName = null; + } + return fileWritten; + } +} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestColdStartLatencyActivity.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestColdStartLatencyActivity.java new file mode 100644 index 00000000..b0202e8e --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestColdStartLatencyActivity.java @@ -0,0 +1,209 @@ +/* + * 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. + */ + +package com.mobileer.oboetester; + +import static com.mobileer.oboetester.TestAudioActivity.TAG; + +import android.content.Context; +import android.media.AudioManager; +import android.os.Bundle; +import android.util.Log; +import android.view.View; +import android.view.WindowManager; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.RadioButton; +import android.widget.Spinner; +import android.widget.TextView; + +import androidx.appcompat.app.AppCompatActivity; + +/** + * Test for getting the cold start latency + */ +public class TestColdStartLatencyActivity extends AppCompatActivity { + + private TextView mStatusView; + private MyStreamSniffer mStreamSniffer; + private AudioManager mAudioManager; + private RadioButton mOutputButton; + private RadioButton mInputButton; + private CheckBox mLowLatencyCheckBox; + private CheckBox mMmapCheckBox; + private CheckBox mExclusiveCheckBox; + private Spinner mStartStabilizeDelaySpinner; + private Spinner mCloseOpenDelaySpinner; + private Spinner mOpenStartDelaySpinner; + private Button mStartButton; + private Button mStopButton; + + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_cold_start_latency); + mStatusView = (TextView) findViewById(R.id.text_status); + mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + + mStartButton = (Button) findViewById(R.id.button_start_test); + mStopButton = (Button) findViewById(R.id.button_stop_test); + mOutputButton = (RadioButton) findViewById(R.id.direction_output); + mInputButton = (RadioButton) findViewById(R.id.direction_input); + mMmapCheckBox = (CheckBox) findViewById(R.id.checkbox_mmap); + mExclusiveCheckBox = (CheckBox) findViewById(R.id.checkbox_exclusive); + mLowLatencyCheckBox = (CheckBox) findViewById(R.id.checkbox_low_latency); + mStartStabilizeDelaySpinner = (Spinner) findViewById(R.id.spinner_start_stabilize_time); + mStartStabilizeDelaySpinner.setSelection(7); // Set to 1000 ms by default + mCloseOpenDelaySpinner = (Spinner) findViewById(R.id.spinner_close_open_time); + mOpenStartDelaySpinner = (Spinner) findViewById(R.id.spinner_open_start_time); + + setButtonsEnabled(false); + } + + public void onStartColdStartLatencyTest(View view) { + keepScreenOn(true); + stopSniffer(); + mStreamSniffer = new MyStreamSniffer(); + mStreamSniffer.start(); + setButtonsEnabled(true); + } + + public void onStopColdStartLatencyTest(View view) { + keepScreenOn(false); + stopSniffer(); + setButtonsEnabled(false); + } + + private void setButtonsEnabled(boolean running) { + mStartButton.setEnabled(!running); + mStopButton.setEnabled(running); + mOutputButton.setEnabled(!running); + mInputButton.setEnabled(!running); + mLowLatencyCheckBox.setEnabled(!running); + mMmapCheckBox.setEnabled(!running); + mExclusiveCheckBox.setEnabled(!running); + mStartStabilizeDelaySpinner.setEnabled(!running); + mCloseOpenDelaySpinner.setEnabled(!running); + mOpenStartDelaySpinner.setEnabled(!running); + } + + protected class MyStreamSniffer extends Thread { + boolean enabled = true; + StringBuffer statusBuffer = new StringBuffer(); + int loopCount; + + @Override + public void run() { + boolean useInput = mInputButton.isChecked(); + boolean useLowLatency = mLowLatencyCheckBox.isChecked(); + boolean useMmap = mMmapCheckBox.isChecked(); + boolean useExclusive = mExclusiveCheckBox.isChecked(); + Log.d(TAG,(useInput ? "IN" : "OUT") + + ", " + (useLowLatency ? "LOW_LATENCY" : "NOT LOW_LATENCY") + + ", " + (useMmap ? "MMAP" : "NOT MMAP") + + ", " + (useExclusive ? "EXCLUSIVE" : "SHARED")); + String closeSleepTimeText = + (String) mCloseOpenDelaySpinner.getAdapter().getItem( + mCloseOpenDelaySpinner.getSelectedItemPosition()); + int closedSleepTimeMillis = Integer.parseInt(closeSleepTimeText); + Log.d(TAG, "Sleep before open time = " + closedSleepTimeMillis + " msec"); + String openSleepTimeText = (String) mOpenStartDelaySpinner.getAdapter().getItem( + mOpenStartDelaySpinner.getSelectedItemPosition()); + int openSleepTimeMillis = Integer.parseInt(openSleepTimeText); + Log.d(TAG, "Sleep after open Time = " + openSleepTimeMillis + " msec"); + String startStabilizeTimeText = (String) mStartStabilizeDelaySpinner.getAdapter().getItem( + mStartStabilizeDelaySpinner.getSelectedItemPosition()); + int startSleepTimeMillis = Integer.parseInt(startStabilizeTimeText); + Log.d(TAG, "Sleep after start Time = " + startSleepTimeMillis + " msec"); + while (enabled) { + loopCount++; + try { + sleep(closedSleepTimeMillis); + openStream(useInput, useLowLatency, useMmap, useExclusive); + log("-------#" + loopCount + " Device Id: " + getAudioDeviceId()); + log("open() Latency: " + getOpenTimeMicros() / 1000 + " msec"); + sleep(openSleepTimeMillis); + startStream(); + log("requestStart() Latency: " + getStartTimeMicros() / 1000 + " msec"); + sleep(startSleepTimeMillis); + log("Cold Start Latency: " + getColdStartTimeMicros() / 1000 + " msec"); + closeStream(); + } catch (InterruptedException e) { + enabled = false; + } finally { + closeStream(); + } + } + } + + // Log to screen and logcat. + private void log(String text) { + statusBuffer.append(text + "\n"); + showStatus(statusBuffer.toString()); + } + + // Stop the test thread. + void finish() { + enabled = false; + interrupt(); + try { + join(2000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + protected void showStatus(final String message) { + runOnUiThread(new Runnable() { + @Override + public void run() { + mStatusView.setText(message); + } + }); + } + + private native int openStream(boolean useInput, boolean useLowLatency, boolean useMmap, + boolean useExclusive); + private native int startStream(); + private native int closeStream(); + private native int getOpenTimeMicros(); + private native int getStartTimeMicros(); + private native int getColdStartTimeMicros(); + private native int getAudioDeviceId(); + + @Override + public void onPause() { + super.onPause(); + stopSniffer(); + } + + private void stopSniffer() { + if (mStreamSniffer != null) { + mStreamSniffer.finish(); + mStreamSniffer = null; + } + } + + protected void keepScreenOn(boolean on) { + if (on) { + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } else { + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + } +} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestDataPathsActivity.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestDataPathsActivity.java new file mode 100644 index 00000000..4b0a9989 --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestDataPathsActivity.java @@ -0,0 +1,984 @@ +/* + * 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. + */ + +package com.mobileer.oboetester; + +import static com.mobileer.oboetester.IntentBasedTestSupport.configureStreamsFromBundle; +import static com.mobileer.oboetester.StreamConfiguration.UNSPECIFIED; +import static com.mobileer.oboetester.StreamConfiguration.convertChannelMaskToText; + +import android.media.AudioDeviceInfo; +import android.media.AudioManager; +import android.os.Build; +import android.os.Bundle; +import android.util.Log; +import android.widget.CheckBox; +import android.widget.RadioButton; +import android.widget.RadioGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; + +import com.mobileer.audio_device.AudioDeviceInfoConverter; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Locale; + +/** + * Play a recognizable tone on each channel of an output device + * and listen for the result through an input. + * Test various channels, InputPresets, ChannelMasks and SampleRates. + * + * Select device types based on priority of attached peripherals. + * Print devices types being tested. + * + * The analysis is based on a cosine transform of a single + * frequency. The magnitude indicates the level. + * The variations in phase, "jitter" indicate how noisy the + * signal is or whether it is corrupted. A very noisy room may have + * lots of energy at the target frequency but the phase will be random. + * + * This test requires a quiet room if you are testing speaker/mic pairs. + * It can also be used to test using analog loopback adapters + * or USB devices configured in loopback mode. + */ +public class TestDataPathsActivity extends BaseAutoGlitchActivity { + + public static final String KEY_USE_INPUT_PRESETS = "use_input_presets"; + public static final boolean VALUE_DEFAULT_USE_INPUT_PRESETS = true; + + public static final String KEY_USE_ALL_SAMPLE_RATES = "use_all_sample_rates"; + public static final boolean VALUE_DEFAULT_USE_ALL_SAMPLE_RATES = false; + + public static final String KEY_SINGLE_TEST_INDEX = "single_test_index"; + public static final int VALUE_DEFAULT_SINGLE_TEST_INDEX = -1; + public static final String KEY_USE_ALL_CHANNEL_COUNTS = "use_all_channel_counts"; + public static final boolean VALUE_DEFAULT_USE_ALL_CHANNEL_COUNTS = true; + public static final String KEY_USE_INPUT_CHANNEL_MASKS = "use_input_channel_masks"; + public static final boolean VALUE_DEFAULT_USE_INPUT_CHANNEL_MASKS = false; + public static final String KEY_OUTPUT_CHANNEL_MASKS_LEVEL = "output_channel_masks_level"; + + // The following KEYs are for old deprecated commands. + public static final String KEY_USE_ALL_OUTPUT_CHANNEL_MASKS = "use_all_output_channel_masks"; + public static final boolean VALUE_DEFAULT_USE_ALL_OUTPUT_CHANNEL_MASKS = false; + + // How many tests should be run in a specific category, eg. channel masks? + private static final int COVERAGE_LEVEL_NONE = 0; + private static final int COVERAGE_LEVEL_SOME = 1; + private static final int COVERAGE_LEVEL_ALL = 2; + + public static final String KEY_USE_INPUT_DEVICES = "use_input_devices"; + public static final boolean VALUE_DEFAULT_USE_INPUT_DEVICES = false; + public static final String KEY_USE_OUTPUT_DEVICES = "use_output_devices"; + public static final boolean VALUE_DEFAULT_USE_OUTPUT_DEVICES = true; + + + public static final int DURATION_SECONDS = 3; + private final static double MIN_REQUIRED_MAGNITUDE = 0.001; + private final static int MAX_SINE_FREQUENCY = 1000; + private final static int TYPICAL_SAMPLE_RATE = 48000; + private final static double FRAMES_PER_CYCLE = TYPICAL_SAMPLE_RATE / MAX_SINE_FREQUENCY; + private final static double PHASE_PER_BIN = 2.0 * Math.PI / FRAMES_PER_CYCLE; + private final static double MAX_ALLOWED_JITTER = 0.5 * PHASE_PER_BIN; + // This must match the value of kPhaseInvalid in BaseSineAnalyzer.h + private final static double PHASE_INVALID = -999.0; + private final static String MAGNITUDE_FORMAT = "%7.5f"; + + // These define the values returned by the Java API deviceInfo.getChannelMasks(). + public static final int JAVA_CHANNEL_IN_LEFT = 1 << 2; // AudioFormat.CHANNEL_IN_LEFT + public static final int JAVA_CHANNEL_IN_RIGHT = 1 << 3; // AudioFormat.CHANNEL_IN_RIGHT + public static final int JAVA_CHANNEL_IN_FRONT = 1 << 4; // AudioFormat.CHANNEL_IN_FRONT + public static final int JAVA_CHANNEL_IN_BACK = 1 << 5; // AudioFormat.CHANNEL_IN_BACK + + // These do not have corresponding Java definitions. + // They match definitions in system/media/audio/include/system/audio-hal-enums.h + public static final int JAVA_CHANNEL_IN_BACK_LEFT = 1 << 16; + public static final int JAVA_CHANNEL_IN_BACK_RIGHT = 1 << 17; + public static final int JAVA_CHANNEL_IN_CENTER = 1 << 18; + public static final int JAVA_CHANNEL_IN_LOW_FREQUENCY = 1 << 20; + public static final int JAVA_CHANNEL_IN_TOP_LEFT = 1 << 21; + public static final int JAVA_CHANNEL_IN_TOP_RIGHT = 1 << 22; + + public static final int JAVA_CHANNEL_IN_MONO = JAVA_CHANNEL_IN_FRONT; + public static final int JAVA_CHANNEL_IN_STEREO = JAVA_CHANNEL_IN_LEFT | JAVA_CHANNEL_IN_RIGHT; + public static final int JAVA_CHANNEL_IN_FRONT_BACK = JAVA_CHANNEL_IN_FRONT | JAVA_CHANNEL_IN_BACK; + public static final int JAVA_CHANNEL_IN_2POINT0POINT2 = JAVA_CHANNEL_IN_LEFT | + JAVA_CHANNEL_IN_RIGHT | + JAVA_CHANNEL_IN_TOP_LEFT | + JAVA_CHANNEL_IN_TOP_RIGHT; + public static final int JAVA_CHANNEL_IN_2POINT1POINT2 = + JAVA_CHANNEL_IN_2POINT0POINT2 | JAVA_CHANNEL_IN_LOW_FREQUENCY; + public static final int JAVA_CHANNEL_IN_3POINT0POINT2 = + JAVA_CHANNEL_IN_2POINT0POINT2 | JAVA_CHANNEL_IN_CENTER; + public static final int JAVA_CHANNEL_IN_3POINT1POINT2 = + JAVA_CHANNEL_IN_3POINT0POINT2 | JAVA_CHANNEL_IN_LOW_FREQUENCY; + public static final int JAVA_CHANNEL_IN_5POINT1 = JAVA_CHANNEL_IN_LEFT | + JAVA_CHANNEL_IN_CENTER | + JAVA_CHANNEL_IN_RIGHT | + JAVA_CHANNEL_IN_BACK_LEFT | + JAVA_CHANNEL_IN_BACK_RIGHT | + JAVA_CHANNEL_IN_LOW_FREQUENCY; + public static final int JAVA_CHANNEL_UNDEFINED = -1; + + final int TYPE_BUILTIN_SPEAKER_SAFE = 0x18; // API 30 + + private double mMagnitude; + private double mMaxMagnitude; + private int mPhaseCount; + private double mPhase; + private double mPhaseErrorSum; + private double mPhaseErrorCount; + + private boolean mSkipRemainingTests; + + private CheckBox mCheckBoxInputPresets; + private CheckBox mCheckBoxAllChannels; + private CheckBox mCheckBoxInputChannelMasks; + private RadioGroup mRadioGroupOutputChannelMasks; + private RadioButton mRadioOutputChannelMasksNone; + private RadioButton mRadioOutputChannelMasksSome; + private RadioButton mRadioOutputChannelMasksAll; + private CheckBox mCheckBoxAllSampleRates; + private TextView mInstructionsTextView; + + private static final int[] INPUT_PRESETS = { + StreamConfiguration.INPUT_PRESET_GENERIC, + StreamConfiguration.INPUT_PRESET_CAMCORDER, + StreamConfiguration.INPUT_PRESET_UNPROCESSED, + // Do not use INPUT_PRESET_VOICE_COMMUNICATION because AEC kills the signal. + StreamConfiguration.INPUT_PRESET_VOICE_RECOGNITION, + StreamConfiguration.INPUT_PRESET_VOICE_PERFORMANCE, + }; + + private static final int[] SHORT_OUTPUT_CHANNEL_MASKS = { + StreamConfiguration.CHANNEL_MONO, + StreamConfiguration.CHANNEL_STEREO, + StreamConfiguration.CHANNEL_2POINT1, // Smallest mask with more than two channels. + StreamConfiguration.CHANNEL_5POINT1, // This mask is very common. + StreamConfiguration.CHANNEL_7POINT1POINT4, // More than 8 channels might break. + }; + + private static final int[] ALL_OUTPUT_CHANNEL_MASKS = { + StreamConfiguration.CHANNEL_MONO, + StreamConfiguration.CHANNEL_STEREO, + StreamConfiguration.CHANNEL_2POINT1, + StreamConfiguration.CHANNEL_TRI, + StreamConfiguration.CHANNEL_TRI_BACK, + StreamConfiguration.CHANNEL_3POINT1, + StreamConfiguration.CHANNEL_2POINT0POINT2, + StreamConfiguration.CHANNEL_2POINT1POINT2, + StreamConfiguration.CHANNEL_3POINT0POINT2, + StreamConfiguration.CHANNEL_3POINT1POINT2, + StreamConfiguration.CHANNEL_QUAD, + StreamConfiguration.CHANNEL_QUAD_SIDE, + StreamConfiguration.CHANNEL_SURROUND, + StreamConfiguration.CHANNEL_PENTA, + StreamConfiguration.CHANNEL_5POINT1, + StreamConfiguration.CHANNEL_5POINT1_SIDE, + StreamConfiguration.CHANNEL_6POINT1, + StreamConfiguration.CHANNEL_7POINT1, + StreamConfiguration.CHANNEL_5POINT1POINT2, + StreamConfiguration.CHANNEL_5POINT1POINT4, + StreamConfiguration.CHANNEL_7POINT1POINT2, + StreamConfiguration.CHANNEL_7POINT1POINT4, + }; + + private static final int[] SAMPLE_RATES = { + 8000, + 11025, + 12000, + 16000, + 22050, + 24000, + 32000, + 44100, + 48000, + 64000, + 88200, + 96000, + }; + + @NonNull + public static String comparePassedField(String prefix, Object failed, Object passed, String name) { + try { + Field field = failed.getClass().getField(name); + int failedValue = field.getInt(failed); + int passedValue = field.getInt(passed); + return (failedValue == passedValue) ? "" + : (prefix + " " + name + ": passed = " + passedValue + ", failed = " + failedValue + "\n"); + } catch (NoSuchFieldException e) { + return "ERROR - no such field " + name; + } catch (IllegalAccessException e) { + return "ERROR - cannot access " + name; + } + } + public static String comparePassedInputPreset(String prefix, TestResult failed, TestResult passed) { + int failedValue = failed.inputPreset; + int passedValue = passed.inputPreset; + return (failedValue == passedValue) ? "" + : (prefix + " inPreset: passed = " + + StreamConfiguration.convertInputPresetToText(passedValue) + + ", failed = " + + StreamConfiguration.convertInputPresetToText(failedValue) + + "\n"); + } + + public static double calculatePhaseError(double p1, double p2) { + double diff = p1 - p2; + // Wrap around the circle. + while (diff > Math.PI) { + diff -= 2 * Math.PI; + } + while (diff < -Math.PI) { + diff += 2 * Math.PI; + } + return diff; + } + + // Periodically query for magnitude and phase from the native detector. + protected class DataPathSniffer extends NativeSniffer { + + @Override + public void startSniffer() { + mMagnitude = -1.0; + mMaxMagnitude = -1.0; + mPhaseCount = 0; + mPhase = 0.0; + mPhaseErrorSum = 0.0; + mPhaseErrorCount = 0; + super.startSniffer(); + } + + private void gatherData() { + mMagnitude = getMagnitude(); + mMaxMagnitude = getMaxMagnitude(); + Log.d(TAG, String.format(Locale.getDefault(), "magnitude = %7.4f, maxMagnitude = %7.4f", + mMagnitude, mMaxMagnitude)); + // Only look at the phase if we have a signal. + if (mMagnitude >= MIN_REQUIRED_MAGNITUDE) { + double phase = getPhaseDataPaths(); + if (phase != PHASE_INVALID) { + // Wait for the analyzer to get a lock on the signal. + // Arbitrary number of phase measurements before we start measuring jitter. + final int kMinPhaseMeasurementsRequired = 4; + if (mPhaseCount >= kMinPhaseMeasurementsRequired) { + double phaseError = Math.abs(calculatePhaseError(phase, mPhase)); + // collect average error + mPhaseErrorSum += phaseError; + mPhaseErrorCount++; + Log.d(TAG, String.format(Locale.getDefault(), "phase = %7.4f, mPhase = %7.4f, phaseError = %7.4f, jitter = %7.4f", + phase, mPhase, phaseError, getAveragePhaseError())); + } + mPhase = phase; + } + mPhaseCount++; + } + } + + public String getCurrentStatusReport() { + StringBuffer message = new StringBuffer(); + message.append( + "magnitude = " + getMagnitudeText(mMagnitude) + + ", max = " + getMagnitudeText(mMaxMagnitude) + + "\nphase = " + getMagnitudeText(mPhase) + + ", jitter = " + getJitterText() + + ", #" + mPhaseCount + + "\n" + + mAutomatedTestRunner.getPassFailReport() + + "\n" + ); + return message.toString(); + } + + public String getShortReport() { + return "maxMag = " + getMagnitudeText(mMaxMagnitude) + + ", jitter = " + getJitterText(); + } + + @Override + public void updateStatusText() { + gatherData(); + mLastGlitchReport = getCurrentStatusReport(); + runOnUiThread(() -> { + setAnalyzerText(mLastGlitchReport); + }); + } + } + + // Write to status and command view + private void setInstructionsText(final String text) { + runOnUiThread(new Runnable() { + @Override + public void run() { + mInstructionsTextView.setText(text); + } + }); + } + + private String getJitterText() { + return isPhaseJitterValid() ? getMagnitudeText(getAveragePhaseError()) : "?"; + } + + @Override + NativeSniffer createNativeSniffer() { + return new TestDataPathsActivity.DataPathSniffer(); + } + + @Override + public String getShortReport() { + return ((DataPathSniffer) mNativeSniffer).getShortReport(); + } + + native double getMagnitude(); + native double getMaxMagnitude(); + + native double getPhaseDataPaths(); + + @Override + protected void inflateActivity() { + setContentView(R.layout.activity_data_paths); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mCheckBoxInputPresets = (CheckBox)findViewById(R.id.checkbox_paths_input_presets); + mCheckBoxAllChannels = (CheckBox)findViewById(R.id.checkbox_paths_all_channels); + mCheckBoxInputChannelMasks = (CheckBox)findViewById(R.id.checkbox_paths_in_channel_masks); + mCheckBoxAllSampleRates = + (CheckBox)findViewById(R.id.checkbox_paths_all_sample_rates); + + mInstructionsTextView = (TextView) findViewById(R.id.text_instructions); + + mRadioGroupOutputChannelMasks = (RadioGroup) findViewById(R.id.group_ch_mask_options); + mRadioOutputChannelMasksNone = (RadioButton) findViewById(R.id.radio_out_ch_masks_none); + mRadioOutputChannelMasksSome = (RadioButton) findViewById(R.id.radio_out_ch_masks_some); + mRadioOutputChannelMasksAll = (RadioButton) findViewById(R.id.radio_out_ch_masks_all); + } + + @Override + public String getTestName() { + return "DataPaths"; + } + + @Override + int getActivityType() { + return ACTIVITY_DATA_PATHS; + } + + static String getMagnitudeText(double value) { + return String.format(Locale.getDefault(), MAGNITUDE_FORMAT, value); + } + + protected String getConfigText(StreamConfiguration config) { + String text = super.getConfigText(config); + if (config.getDirection() == StreamConfiguration.DIRECTION_INPUT) { + text += ", inPre = " + StreamConfiguration.convertInputPresetToText(config.getInputPreset()); + } + return text; + } + + @Override + protected String whyShouldTestBeSkipped() { + String why = super.whyShouldTestBeSkipped(); + StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration; + StreamConfiguration requestedOutConfig = mAudioOutTester.requestedConfiguration; + StreamConfiguration actualInConfig = mAudioInputTester.actualConfiguration; + StreamConfiguration actualOutConfig = mAudioOutTester.actualConfiguration; + + // Did we request a device and not get that device? + if (requestedInConfig.getDeviceId() != 0 + && (requestedInConfig.getDeviceId() != actualInConfig.getDeviceId())) { + why += "inDev(" + requestedInConfig.getDeviceId() + + "!=" + actualInConfig.getDeviceId() + "),"; + mSkipRemainingTests = true; // the device must have been unplugged + } + if (requestedOutConfig.getDeviceId() != 0 + && (requestedOutConfig.getDeviceId() != actualOutConfig.getDeviceId())) { + why += ", outDev(" + requestedOutConfig.getDeviceId() + + "!=" + actualOutConfig.getDeviceId() + "),"; + mSkipRemainingTests = true; // the device must have been unplugged + } + if ((requestedInConfig.getInputPreset() != actualInConfig.getInputPreset())) { + why += ", inPre(" + requestedInConfig.getInputPreset() + + "!=" + actualInConfig.getInputPreset() + "),"; + } + return why; + } + + @Override + protected boolean isFinishedEarly() { + return (mMaxMagnitude > MIN_REQUIRED_MAGNITUDE) + && (getAveragePhaseError() < MAX_ALLOWED_JITTER) + && isPhaseJitterValid(); + } + + // @return reasons for failure of empty string + @Override + public String didTestFail() { + String why = ""; + if (mMaxMagnitude <= MIN_REQUIRED_MAGNITUDE) { + why += ", mag"; + } + if (!isPhaseJitterValid()) { + why += ", jitterUnknown"; + } else if (getAveragePhaseError() > MAX_ALLOWED_JITTER) { + why += ", jitterHigh"; + } + return why; + } + + private double getAveragePhaseError() { + // If we have no measurements then return maximum possible phase jitter + // to avoid dividing by zero. + return (mPhaseErrorCount > 0) ? (mPhaseErrorSum / mPhaseErrorCount) : Math.PI; + } + + private boolean isPhaseJitterValid() { + // Arbitrary number of measurements to be considered valid. + final int kMinPhaseErrorCount = 5; + return mPhaseErrorCount >= kMinPhaseErrorCount; + } + + String getOneLineSummary() { + StreamConfiguration actualInConfig = mAudioInputTester.actualConfiguration; + StreamConfiguration actualOutConfig = mAudioOutTester.actualConfiguration; + return "#" + mAutomatedTestRunner.getTestCount() + + ", IN" + (actualInConfig.isMMap() ? "-M" : "-L") + + " D=" + actualInConfig.getDeviceId() + + ", ch=" + channelText(getInputChannel(), actualInConfig.getChannelCount()) + + ", SR=" + actualInConfig.getSampleRate() + + ", OUT" + (actualOutConfig.isMMap() ? "-M" : "-L") + + " D=" + actualOutConfig.getDeviceId() + + ", ch=" + channelText(getOutputChannel(), actualOutConfig.getChannelCount()) + + ", SR=" + actualOutConfig.getSampleRate() + + ", mag = " + getMagnitudeText(mMaxMagnitude) + + ", jitter = " + getJitterText(); + } + + @Override + protected TestResult testCurrentConfigurations() throws InterruptedException { + if (mSkipRemainingTests) { + throw new DeviceUnpluggedException(); + } + TestResult testResult = super.testCurrentConfigurations(); + if (testResult != null) { + testResult.addComment("mag = " + TestDataPathsActivity.getMagnitudeText(mMagnitude) + + ", jitter = " + getJitterText()); + + logOneLineSummary(testResult); + int result = testResult.result; + if (result == TEST_RESULT_FAILED) { + int id = mAudioOutTester.actualConfiguration.getDeviceId(); + int channelCount = mAudioOutTester.actualConfiguration.getChannelCount(); + AudioDeviceInfo info = getDeviceInfoById(id); + if (info != null) { + int deviceType = getDeviceInfoById(id).getType(); + if (deviceType == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE + && channelCount == 2 + && getOutputChannel() == 1) { + testResult.addComment("Maybe EARPIECE does not mix stereo to mono!"); + } + if (deviceType == TYPE_BUILTIN_SPEAKER_SAFE + && channelCount == 2 + && getOutputChannel() == 0) { + testResult.addComment("Maybe SPEAKER_SAFE dropped channel zero!"); + } + } + } + } + return testResult; + } + + private void logSection(String name) { + logBoth("\n#" + (getTestCount() + 1) + " ########### " + name + "\n"); + } + + private void testInputPresets() throws InterruptedException { + logSection("InputPreset"); + StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration; + int originalPreset = requestedInConfig.getInputPreset(); + for (int inputPreset : INPUT_PRESETS) { + requestedInConfig.setInputPreset(inputPreset); + testPerformancePaths(); + } + requestedInConfig.setInputPreset(originalPreset); + } + + // The native out channel mask is its channel mask shifted right by 2 bits. + // See AudioFormat.convertChannelOutMaskToNativeMask() + int convertJavaOutChannelMaskToNativeChannelMask(int javaChannelMask) { + return javaChannelMask >> 2; + } + + // The native channel mask in AAudio and Oboe is different than the Java IN channel mask. + // See AAudioConvert_aaudioToAndroidChannelLayoutMask() + int convertJavaInChannelMaskToNativeChannelMask(int javaChannelMask) { + switch (javaChannelMask) { + case JAVA_CHANNEL_IN_MONO: + return StreamConfiguration.CHANNEL_MONO; + case JAVA_CHANNEL_IN_STEREO: + return StreamConfiguration.CHANNEL_STEREO; + case JAVA_CHANNEL_IN_FRONT_BACK: + return StreamConfiguration.CHANNEL_FRONT_BACK; + case JAVA_CHANNEL_IN_2POINT0POINT2: + return StreamConfiguration.CHANNEL_2POINT0POINT2; + case JAVA_CHANNEL_IN_2POINT1POINT2: + return StreamConfiguration.CHANNEL_2POINT1POINT2; + case JAVA_CHANNEL_IN_3POINT0POINT2: + return StreamConfiguration.CHANNEL_3POINT0POINT2; + case JAVA_CHANNEL_IN_3POINT1POINT2: + return StreamConfiguration.CHANNEL_3POINT1POINT2; + case JAVA_CHANNEL_IN_5POINT1: + return StreamConfiguration.CHANNEL_5POINT1; + default: + log("Unimplemented java channel mask: " + javaChannelMask + "\n"); + return JAVA_CHANNEL_UNDEFINED; + } + } + + void logOneLineSummary(TestResult testResult) { + int result = testResult.result; + String oneLineSummary; + if (result == TEST_RESULT_SKIPPED) { + oneLineSummary = "#" + mAutomatedTestRunner.getTestCount() + ", SKIP"; + } else if (result == TEST_RESULT_FAILED) { + oneLineSummary = getOneLineSummary() + ", FAIL"; + } else { + oneLineSummary = getOneLineSummary(); + } + appendSummary(oneLineSummary + "\n"); + } + + void logBoth(String text) { + log(text); + appendSummary(text + "\n"); + } + + private void testDeviceOutputInfo(AudioDeviceInfo outputDeviceInfo) throws InterruptedException { + AudioDeviceInfo inputDeviceInfo = findCompatibleInputDevice(outputDeviceInfo.getType()); + showDeviceInfo(outputDeviceInfo, inputDeviceInfo); + if (inputDeviceInfo == null) { + return; + } + + StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration; + StreamConfiguration requestedOutConfig = mAudioOutTester.requestedConfiguration; + requestedInConfig.reset(); + requestedOutConfig.reset(); + requestedInConfig.setDeviceId(inputDeviceInfo.getId()); + requestedOutConfig.setDeviceId(outputDeviceInfo.getId()); + resetChannelConfigurations(requestedInConfig, requestedOutConfig); + + testBug_270535408(inputDeviceInfo, outputDeviceInfo); + + if (mCheckBoxAllChannels.isChecked()) { + runOnUiThread(() -> mCheckBoxAllChannels.setEnabled(false)); + testOutputChannelCounts(inputDeviceInfo, outputDeviceInfo); + } + + if (mCheckBoxInputPresets.isChecked()) { + runOnUiThread(() -> mCheckBoxInputPresets.setEnabled(false)); + testInputPresets(); + } + + if (mCheckBoxAllSampleRates.isChecked()) { + logSection("Sample Rates"); + runOnUiThread(() -> mCheckBoxAllSampleRates.setEnabled(false)); + for (int sampleRate : SAMPLE_RATES) { + requestedInConfig.setSampleRate(sampleRate); + requestedOutConfig.setSampleRate(sampleRate); + testPerformancePaths(); + } + } + requestedInConfig.setSampleRate(0); + requestedOutConfig.setSampleRate(0); + + // Channel Masks added to AAudio API in S_V2 + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.S_V2 + && isDeviceTypeMixedForLoopback(outputDeviceInfo.getType())) { + + if (mCheckBoxInputChannelMasks.isChecked()) { // INPUT? + logSection("Input Channel Masks"); + runOnUiThread(() -> mCheckBoxInputChannelMasks.setEnabled(false)); + + resetChannelConfigurations(requestedInConfig, requestedOutConfig); + requestedInConfig.setChannelCount(0); + // Test the reported channel masks. + int[] channelMasks = inputDeviceInfo.getChannelMasks(); + if (channelMasks.length > 0) { + for (int channelMask : channelMasks) { + int nativeChannelMask = + convertJavaInChannelMaskToNativeChannelMask(channelMask); + if (nativeChannelMask == JAVA_CHANNEL_UNDEFINED) { + log("channelMask: " + channelMask + " not supported. Skipping.\n"); + continue; + } + log("\n#### nativeChannelMask = " + + convertChannelMaskToText(nativeChannelMask) + "\n"); + int channelCount = Integer.bitCount(nativeChannelMask); + requestedInConfig.setChannelMask(nativeChannelMask); + for (int channel = 0; channel < channelCount; channel++) { + setInputChannel(channel); + testPerformancePaths(); + } + } + } + resetChannelConfigurations(requestedInConfig, requestedOutConfig); + } + + runOnUiThread(() -> mRadioGroupOutputChannelMasks.setEnabled(false)); + if (!mRadioOutputChannelMasksNone.isChecked()) { // OUTPUT? + logSection("Output Channel Masks"); + requestedInConfig.setChannelCount(1); + for (int channelMask : mRadioOutputChannelMasksAll.isChecked() ? + ALL_OUTPUT_CHANNEL_MASKS : SHORT_OUTPUT_CHANNEL_MASKS) { + int channelCount = Integer.bitCount(channelMask); + requestedOutConfig.setChannelMask(channelMask); + for (int channel = 0; channel < channelCount; channel++) { + setOutputChannel(channel); + testPerformancePaths(); + } + } + resetChannelConfigurations(requestedInConfig, requestedOutConfig); + } + } + } + + private void resetChannelConfigurations(StreamConfiguration requestedInConfig, StreamConfiguration requestedOutConfig) { + requestedInConfig.setChannelMask(0); + requestedOutConfig.setChannelMask(0); + requestedInConfig.setChannelCount(1); + requestedOutConfig.setChannelCount(1); + setInputChannel(0); + setOutputChannel(0); + } + + private void showDeviceInfo(AudioDeviceInfo outputDeviceInfo, AudioDeviceInfo inputDeviceInfo) { + String deviceText = "OUT: type = " + + AudioDeviceInfoConverter.typeToString(outputDeviceInfo.getType()) + + ", #ch = " + findLargestChannelCount(outputDeviceInfo.getChannelCounts()); + + setInstructionsText(deviceText); + + if (inputDeviceInfo == null) { + deviceText += "\nERROR - no compatible input device!"; + } else { + deviceText = "IN: type = " + + AudioDeviceInfoConverter.typeToString(inputDeviceInfo.getType()) + + ", #ch = " + findLargestChannelCount(inputDeviceInfo.getChannelCounts()) + + "\n" + deviceText; + } + setInstructionsText(deviceText); + } + + public static int findLargestChannelCount(int[] arr) { + if (arr == null || arr.length == 0) { + return 2; + } + return findLargestInt(arr); + } + + public static int findLargestInt(int[] arr) { + if (arr == null || arr.length == 0) { + throw new IllegalArgumentException("Array cannot be empty"); + } + + int max = arr[0]; + for (int i = 1; i < arr.length; i++) { + if (arr[i] > max) { + max = arr[i]; + } + } + return max; + } + + private void testOutputChannelCounts(AudioDeviceInfo inputDeviceInfo, AudioDeviceInfo outputDeviceInfo) throws InterruptedException { + final int maxOutputChannelsToTest = 4; // takes too long + logSection("Output Channel Counts"); + ArrayList channelCountsTested = new ArrayList(); + StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration; + StreamConfiguration requestedOutConfig = mAudioOutTester.requestedConfiguration; + + int[] outputChannelCounts = outputDeviceInfo.getChannelCounts(); + // Are the output channels mixed together in the air or in a loopback plug? + if (isDeviceTypeMixedForLoopback(outputDeviceInfo.getType())) { + requestedInConfig.setChannelCount(1); + setInputChannel(0); + // test mono + requestedOutConfig.setChannelCount(1); + channelCountsTested.add(1); + setOutputChannel(0); + testPerformancePaths(); + // test stereo + requestedOutConfig.setChannelCount(2); + channelCountsTested.add(2); + setOutputChannel(0); + testPerformancePaths(); + setOutputChannel(1); + testPerformancePaths(); + + // Test channels for each channelCount above 2 + for (int numChannels : outputChannelCounts) { + if (numChannels > maxOutputChannelsToTest) { + log("skip numChannels = " + numChannels); + } else { + if (!channelCountsTested.contains(numChannels)) { + log("--- test numChannels = " + numChannels); + requestedOutConfig.setChannelCount(numChannels); + channelCountsTested.add(numChannels); + for (int channel = 0; channel < numChannels; channel++) { + setOutputChannel(channel); + testPerformancePaths(); + } + } + } + + } + } else { + // This device does not mix so we have to match the input and output channel indices. + // Find the maximum number of input channels. + int[] inputChannelCounts = inputDeviceInfo.getChannelCounts(); + int maxInputChannels = findLargestChannelCount(inputChannelCounts); + int maxInputChannelsToTest = Math.min(maxOutputChannelsToTest, maxInputChannels); + // test mono + testMatchingChannels(1); + channelCountsTested.add(1); + // Test two matching stereo channels. + testMatchingChannels(2); + channelCountsTested.add(2); + // Test matching channels for each channelCount above 2 + for (int numChannels : outputChannelCounts) { + if (numChannels > maxInputChannelsToTest) { + log("skip numChannels = " + numChannels + " because > #inputs"); + } else { + if (!channelCountsTested.contains(numChannels)) { + log("--- test numChannels = " + numChannels); + testMatchingChannels(numChannels); + channelCountsTested.add(numChannels); + } + } + } + } + // Restore defaults. + requestedInConfig.setChannelCount(1); + setInputChannel(0); + requestedOutConfig.setChannelCount(1); + setOutputChannel(0); + } + + private void testMatchingChannels(int numChannels) throws InterruptedException { + mAudioInputTester.requestedConfiguration.setChannelCount(numChannels); + mAudioOutTester.requestedConfiguration.setChannelCount(numChannels); + for (int channel = 0; channel < numChannels; channel++) { + setInputChannel(channel); + setOutputChannel(channel); + testPerformancePaths(); + } + } + + // b/270535408 | no input when channels=3 and sessionId is allocated + private void testBug_270535408(AudioDeviceInfo inputDeviceInfo, + AudioDeviceInfo outputDeviceInfo) throws InterruptedException { + int[] inputChannelCounts = inputDeviceInfo.getChannelCounts(); + if (findLargestChannelCount(inputChannelCounts) >= 3) { + logSection("Bug 270535408, 3ch + SessionId"); + StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration; + StreamConfiguration requestedOutConfig = mAudioOutTester.requestedConfiguration; + requestedInConfig.setChannelCount(3); + requestedInConfig.setSessionId(AudioManager.AUDIO_SESSION_ID_GENERATE); + requestedInConfig.setPerformanceMode(StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY); + requestedOutConfig.setPerformanceMode(StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY); + testCurrentConfigurations(); + // Now test without a sessionId so we have a passing test to compare with. + requestedInConfig.setSessionId(-1); // AAUDIO_SESSION_ID_NONE + testCurrentConfigurations(); + requestedInConfig.setChannelCount(UNSPECIFIED); + } + } + + private void testPerformancePaths() throws InterruptedException { + StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration; + StreamConfiguration requestedOutConfig = mAudioOutTester.requestedConfiguration; + + requestedInConfig.setSharingMode(StreamConfiguration.SHARING_MODE_SHARED); + requestedOutConfig.setSharingMode(StreamConfiguration.SHARING_MODE_SHARED); + + // Legacy NONE + requestedInConfig.setMMap(false); + requestedOutConfig.setMMap(false); + requestedInConfig.setPerformanceMode(StreamConfiguration.PERFORMANCE_MODE_NONE); + requestedOutConfig.setPerformanceMode(StreamConfiguration.PERFORMANCE_MODE_NONE); + testCurrentConfigurations(); + + // Legacy LOW_LATENCY + requestedInConfig.setPerformanceMode(StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY); + requestedOutConfig.setPerformanceMode(StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY); + testCurrentConfigurations(); + + // MMAP LowLatency + if (NativeEngine.isMMapSupported()) { + requestedInConfig.setMMap(true); + requestedOutConfig.setMMap(true); + testCurrentConfigurations(); + } + requestedInConfig.setMMap(false); + requestedOutConfig.setMMap(false); + + } + + private void testOutputDeviceTypes() throws InterruptedException { + // Determine which output device type to test based on priorities. + AudioDeviceInfo info = getDeviceInfoByType(AudioDeviceInfo.TYPE_USB_DEVICE, + AudioManager.GET_DEVICES_OUTPUTS); + if (info != null) { + testDeviceOutputInfo(info); + return; + } + info = getDeviceInfoByType(AudioDeviceInfo.TYPE_USB_HEADSET, + AudioManager.GET_DEVICES_OUTPUTS); + if (info != null) { + testDeviceOutputInfo(info); + return; + } + info = getDeviceInfoByType(AudioDeviceInfo.TYPE_WIRED_HEADSET, + AudioManager.GET_DEVICES_OUTPUTS); + if (info != null) { + testDeviceOutputInfo(info); + return; + } + // Test both SPEAKER and SPEAKER_SAFE + info = getDeviceInfoByType(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, + AudioManager.GET_DEVICES_OUTPUTS); + if (info != null) { + testDeviceOutputInfo(info); + // Continue on to SPEAKER_SAFE + } + info = getDeviceInfoByType(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER_SAFE, + AudioManager.GET_DEVICES_OUTPUTS); + if (info != null) { + testDeviceOutputInfo(info); + } + } + + class DeviceUnpluggedException extends RuntimeException { + public DeviceUnpluggedException() { + super("Device was unplugged."); + } + } + + @Override + public void runTest() { + try { + logDeviceInfo(); + log("min.required.magnitude = " + MIN_REQUIRED_MAGNITUDE); + log("max.allowed.jitter = " + MAX_ALLOWED_JITTER); + log("test.gap.msec = " + mGapMillis); + + mTestResults.clear(); + mDurationSeconds = DURATION_SECONDS; + + runOnUiThread(() -> keepScreenOn(true)); + + mSkipRemainingTests = false; + try { + testOutputDeviceTypes(); + } catch(DeviceUnpluggedException e) { + log("Remaining tests were skipped, " + e.getMessage()); + } + + compareFailedTestsWithNearestPassingTest(); + + } catch (InterruptedException e) { + compareFailedTestsWithNearestPassingTest(); + } catch (Exception e) { + log(e.getMessage()); + showErrorToast(e.getMessage()); + } finally { + runOnUiThread(() -> { + mCheckBoxInputPresets.setEnabled(true); + mCheckBoxAllChannels.setEnabled(true); + mCheckBoxInputChannelMasks.setEnabled(true); + mRadioGroupOutputChannelMasks.setEnabled(true); + mCheckBoxAllSampleRates.setEnabled(true); + keepScreenOn(false); + }); + } + } + + @Override + public void startTestUsingBundle() { + StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration; + StreamConfiguration requestedOutConfig = mAudioOutTester.requestedConfiguration; + configureStreamsFromBundle(mBundleFromIntent, requestedInConfig, requestedOutConfig); + + // These are the current supported options. + final boolean shouldUseInputPresets = mBundleFromIntent.getBoolean(KEY_USE_INPUT_PRESETS, + VALUE_DEFAULT_USE_INPUT_PRESETS); + final boolean shouldUseAllSampleRates = + mBundleFromIntent.getBoolean(KEY_USE_ALL_SAMPLE_RATES, + VALUE_DEFAULT_USE_ALL_SAMPLE_RATES); + + final int singleTestIndex = mBundleFromIntent.getInt(KEY_SINGLE_TEST_INDEX, + VALUE_DEFAULT_SINGLE_TEST_INDEX); + + // The old deprecated commands will get mapped to the closest new options. + final boolean shouldUseInputDevices = mBundleFromIntent.getBoolean(KEY_USE_INPUT_DEVICES, + VALUE_DEFAULT_USE_INPUT_CHANNEL_MASKS); + final boolean shouldUseInputChannelMasks = + mBundleFromIntent.getBoolean(KEY_USE_INPUT_CHANNEL_MASKS, + shouldUseInputDevices); + + final boolean shouldUseOutputDevices = mBundleFromIntent.getBoolean(KEY_USE_OUTPUT_DEVICES, + VALUE_DEFAULT_USE_ALL_CHANNEL_COUNTS); + final boolean shouldUseAllChannelCounts = + mBundleFromIntent.getBoolean(KEY_USE_ALL_CHANNEL_COUNTS, + shouldUseOutputDevices); + + final boolean shouldUseAllOutputChannelMasks = + mBundleFromIntent.getBoolean(KEY_USE_ALL_OUTPUT_CHANNEL_MASKS, + VALUE_DEFAULT_USE_ALL_OUTPUT_CHANNEL_MASKS); + final int defaultOutputChannelMasksLevel = shouldUseAllOutputChannelMasks + ? COVERAGE_LEVEL_ALL : COVERAGE_LEVEL_SOME; + final int outputChannelMasksLevel = mBundleFromIntent.getInt(KEY_OUTPUT_CHANNEL_MASKS_LEVEL, + defaultOutputChannelMasksLevel); + + runOnUiThread(() -> { + mCheckBoxInputPresets.setChecked(shouldUseInputPresets); + mCheckBoxAllSampleRates.setChecked(shouldUseAllSampleRates); + mAutomatedTestRunner.setTestIndexText(singleTestIndex); + mCheckBoxAllChannels.setChecked(shouldUseAllChannelCounts); + mCheckBoxInputChannelMasks.setChecked(shouldUseInputChannelMasks); + switch(outputChannelMasksLevel) { + case COVERAGE_LEVEL_ALL: + mRadioOutputChannelMasksAll.setChecked(true); + break; + case COVERAGE_LEVEL_SOME: + mRadioOutputChannelMasksSome.setChecked(true); + break; + case COVERAGE_LEVEL_NONE: + default: + mRadioOutputChannelMasksNone.setChecked(true); + break; + } + }); + + // This will sync with the above checkbox code because it will log on the UI + // thread before running any tests. + mAutomatedTestRunner.startTest(); + } +} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestDisconnectActivity.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestDisconnectActivity.java new file mode 100644 index 00000000..458a3ef0 --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestDisconnectActivity.java @@ -0,0 +1,566 @@ +/* + * 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. + */ + +package com.mobileer.oboetester; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.hardware.usb.UsbConstants; +import android.hardware.usb.UsbDevice; +import android.hardware.usb.UsbInterface; +import android.hardware.usb.UsbManager; +import android.os.Bundle; +import android.util.Log; +import android.view.View; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.TextView; + +import java.io.IOException; + +/** + * Guide the user through a series of tests plugging in and unplugging a headset. + * Print a summary at the end of any failures. + */ +public class TestDisconnectActivity extends TestAudioActivity { + + private static final String TEXT_SKIP = "SKIP"; + private static final String TEXT_PASS = "PASS"; + private static final String TEXT_FAIL = "FAIL !!!!"; + public static final int POLL_DURATION_MILLIS = 50; + public static final int SETTLING_TIME_MILLIS = 600; + public static final int TIME_TO_FAILURE_MILLIS = 3000; + + private TextView mInstructionsTextView; + private TextView mStatusTextView; + private TextView mPlugTextView; + + private volatile boolean mTestFailed; + private volatile boolean mSkipTest; + private volatile int mPlugCount; + private volatile int mUsbDeviceAttachedCount; + private volatile int mPlugState; + private volatile int mPlugMicrophone; + private BroadcastReceiver mPluginReceiver = new PluginBroadcastReceiver(); + private Button mFailButton; + private Button mSkipButton; + private CheckBox mCheckBoxInputs; + private CheckBox mCheckBoxOutputs; + + protected AutomatedTestRunner mAutomatedTestRunner; + + // Receive a broadcast Intent when a headset is plugged in or unplugged. + // Display a count on screen. + public class PluginBroadcastReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + switch (intent.getAction()) { + case Intent.ACTION_HEADSET_PLUG: { + mPlugMicrophone = intent.getIntExtra("microphone", -1); + mPlugState = intent.getIntExtra("state", -1); + mPlugCount++; + } break; + case UsbManager.ACTION_USB_DEVICE_ATTACHED: + case UsbManager.ACTION_USB_DEVICE_DETACHED: { + UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); + final boolean hasAudioPlayback = + containsAudioStreamingInterface(device, UsbConstants.USB_DIR_OUT); + final boolean hasAudioCapture = + containsAudioStreamingInterface(device, UsbConstants.USB_DIR_IN); + if (hasAudioPlayback || hasAudioCapture) { + mPlugState = + intent.getAction() == UsbManager.ACTION_USB_DEVICE_ATTACHED ? 1 : 0; + mUsbDeviceAttachedCount++; + mPlugMicrophone = hasAudioCapture ? 1 : 0; + } + } break; + default: + break; + } + runOnUiThread(new Runnable() { + @Override + public void run() { + String message = "HEADSET_PLUG #" + mPlugCount + + ", USB_DEVICE_DE/ATTACHED #" + mUsbDeviceAttachedCount + + ", mic = " + mPlugMicrophone + + ", state = " + mPlugState; + mPlugTextView.setText(message); + log(message); + } + }); + } + + private static final int AUDIO_STREAMING_SUB_CLASS = 2; + + /** + * Figure out if an UsbDevice contains audio input/output streaming interface or not. + * + * @param device the given UsbDevice + * @param direction the direction of the audio streaming interface + * @return true if the UsbDevice contains the audio input/output streaming interface. + */ + private boolean containsAudioStreamingInterface(UsbDevice device, int direction) { + final int interfaceCount = device.getInterfaceCount(); + for (int i = 0; i < interfaceCount; ++i) { + UsbInterface usbInterface = device.getInterface(i); + if (usbInterface.getInterfaceClass() != UsbConstants.USB_CLASS_AUDIO + && usbInterface.getInterfaceSubclass() != AUDIO_STREAMING_SUB_CLASS) { + continue; + } + final int endpointCount = usbInterface.getEndpointCount(); + for (int j = 0; j < endpointCount; ++j) { + if (usbInterface.getEndpoint(j).getDirection() == direction) { + return true; + } + } + } + return false; + } + } + + @Override + protected void inflateActivity() { + setContentView(R.layout.activity_test_disconnect); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mAutomatedTestRunner = findViewById(R.id.auto_test_runner); + mAutomatedTestRunner.setActivity(this); + + mInstructionsTextView = (TextView) findViewById(R.id.text_instructions); + mStatusTextView = (TextView) findViewById(R.id.text_status); + mPlugTextView = (TextView) findViewById(R.id.text_plug_events); + + mCheckBoxInputs = (CheckBox)findViewById(R.id.checkbox_disco_inputs); + mCheckBoxOutputs = (CheckBox)findViewById(R.id.checkbox_disco_outputs); + + mFailButton = (Button) findViewById(R.id.button_fail); + mSkipButton = (Button) findViewById(R.id.button_skip); + updateFailSkipButton(false); + } + + @Override + public String getTestName() { + return "Disconnect"; + } + + int getActivityType() { + return ACTIVITY_TEST_DISCONNECT; + } + + @Override + boolean isOutput() { + return true; + } + + private void updateFailSkipButton(final boolean running) { + runOnUiThread(new Runnable() { + @Override + public void run() { + mFailButton.setEnabled(running); + mSkipButton.setEnabled(running); + } + }); + } + + // Write to status and command view + private void setInstructionsText(final String text) { + runOnUiThread(new Runnable() { + @Override + public void run() { + mInstructionsTextView.setText(text); + } + }); + } + + // Write to status and command view + private void setStatusText(final String text) { + runOnUiThread(new Runnable() { + @Override + public void run() { + mStatusTextView.setText(text); + } + }); + } + + @Override + public void onResume() { + super.onResume(); + IntentFilter filter = new IntentFilter(Intent.ACTION_HEADSET_PLUG); + filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED); + filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED); + this.registerReceiver(mPluginReceiver, filter); + } + + @Override + public void onPause() { + this.unregisterReceiver(mPluginReceiver); + super.onPause(); + } + + // This should only be called from UI events such as onStop or a button press. + @Override + public void onStopTest() { + mAutomatedTestRunner.stopTest(); + } + + public void startAudioTest() throws IOException { + startAudio(); + } + + public void stopAudioTest() { + stopAudioQuiet(); + closeAudio(); + } + + public void onCancel(View view) { + stopAudioTest(); + mAutomatedTestRunner.onTestFinished(); + } + + // Called on UI thread + public void onStopAudioTest(View view) { + stopAudioTest(); + mAutomatedTestRunner.onTestFinished(); + keepScreenOn(false); + } + + public void onFailTest(View view) { + mTestFailed = true; + } + + public void onSkipTest(View view) { + mSkipTest = true; + } + + private String getConfigText(StreamConfiguration config) { + return ((config.getDirection() == StreamConfiguration.DIRECTION_OUTPUT) ? "OUT" : "IN") + + ", Perf = " + StreamConfiguration.convertPerformanceModeToText( + config.getPerformanceMode()) + + ", " + StreamConfiguration.convertSharingModeToText(config.getSharingMode()) + + ", " + config.getSampleRate() + + ", SRC = " + StreamConfiguration.convertRateConversionQualityToText(config.getRateConversionQuality()); + } + + private void log(Exception e) { + Log.e(TestAudioActivity.TAG, "Caught ", e); + mAutomatedTestRunner.log("Caught " + e); + } + + private void log(String text) { + mAutomatedTestRunner.log(text); + } + + private void flushLog() { + mAutomatedTestRunner.flushLog(); + } + + private void appendFailedSummary(String text) { + mAutomatedTestRunner.appendFailedSummary(text); + } + + private void testConfiguration(boolean isInput, + int perfMode, + int sharingMode, + int sampleRate, + int sampleRateConversionQuality, + boolean requestPlugin) throws InterruptedException { + if ((getSingleTestIndex() >= 0) && (mAutomatedTestRunner.getTestCount() != getSingleTestIndex())) { + mAutomatedTestRunner.incrementTestCount(); + return; + } + + if (!isInput && !mCheckBoxOutputs.isChecked()) { + return; + } + if (isInput && !mCheckBoxInputs.isChecked()) { + return; + } + + updateFailSkipButton(true); + + String actualConfigText = "none"; + mSkipTest = false; + mTestFailed = false; + + // Try to synchronize with the current headset state, IN or OUT. + while (mAutomatedTestRunner.isThreadEnabled() && !mSkipTest && !mTestFailed) { + if (requestPlugin != (mPlugState == 0)) { + String message = "SYNC: " + (requestPlugin ? "UNplug" : "Plug IN") + " headset now!"; + setInstructionsText(message); + Thread.sleep(POLL_DURATION_MILLIS); + } else { + break; + } + } + + AudioInputTester mAudioInTester = null; + AudioOutputTester mAudioOutTester = null; + + clearStreamContexts(); + + if (isInput) { + mAudioInTester = addAudioInputTester(); + } else { + mAudioOutTester = addAudioOutputTester(); + } + + // Configure settings + StreamConfiguration requestedConfig = (isInput) + ? mAudioInTester.requestedConfiguration + : mAudioOutTester.requestedConfiguration; + StreamConfiguration actualConfig = (isInput) + ? mAudioInTester.actualConfiguration + : mAudioOutTester.actualConfiguration; + + requestedConfig.reset(); + requestedConfig.setPerformanceMode(perfMode); + requestedConfig.setSharingMode(sharingMode); + requestedConfig.setSampleRate(sampleRate); + + if (sampleRate != 0) { + requestedConfig.setRateConversionQuality(sampleRateConversionQuality); + } + + log("========================== #" + mAutomatedTestRunner.getTestCount()); + log("Requested:"); + log(getConfigText(requestedConfig)); + + // Give previous stream time to close and release resources. Avoid race conditions. + Thread.sleep(SETTLING_TIME_MILLIS); + if (!mAutomatedTestRunner.isThreadEnabled()) return; + boolean openFailed = false; + boolean hasMicFailed = false; + AudioStreamBase stream = null; + try { + openAudio(); + log("Actual:"); + actualConfigText = getConfigText(actualConfig) + + ", " + ((actualConfig.isMMap() ? "MMAP" : "Legacy") + + ", Dev = " + actualConfig.getDeviceId() + ); + log(actualConfigText); + flushLog(); + + stream = (isInput) + ? mAudioInTester.getCurrentAudioStream() + : mAudioOutTester.getCurrentAudioStream(); + } catch (IOException e) { + openFailed = true; + log(e); + } + + // The test is only worth running if we got the configuration we requested. + boolean valid = true; + if (!openFailed) { + if(actualConfig.getSharingMode() != sharingMode) { + log("did not get requested sharing mode"); + valid = false; + } + if (actualConfig.getPerformanceMode() != perfMode) { + log("did not get requested performance mode"); + valid = false; + } + if (actualConfig.getNativeApi() == StreamConfiguration.NATIVE_API_OPENSLES) { + log("OpenSL ES does not support automatic disconnect"); + valid = false; + } + } + + if (!openFailed && valid) { + try { + startAudioTest(); + } catch (IOException e) { + e.printStackTrace(); + valid = false; + log(e); + } + } + + int oldPlugCount = mPlugCount; + if (!openFailed && valid) { + mTestFailed = false; + // poll until stream started + while (!mTestFailed && mAutomatedTestRunner.isThreadEnabled() && !mSkipTest && + stream.getState() == StreamConfiguration.STREAM_STATE_STARTING) { + Thread.sleep(POLL_DURATION_MILLIS); + } + String message = (requestPlugin ? "Plug IN" : "UNplug") + " headset now!"; + setStatusText("Testing:\n" + actualConfigText); + setInstructionsText(message); + int timeoutCount = 0; + // Wait for Java plug count to change or stream to disconnect. + while (!mTestFailed && mAutomatedTestRunner.isThreadEnabled() && !mSkipTest && + stream.getState() == StreamConfiguration.STREAM_STATE_STARTED) { + flushLog(); + Thread.sleep(POLL_DURATION_MILLIS); + if (mPlugCount > oldPlugCount) { + timeoutCount = TIME_TO_FAILURE_MILLIS / POLL_DURATION_MILLIS; + break; + } + } + + // Wait for timeout or stream to disconnect. + while (!mTestFailed && mAutomatedTestRunner.isThreadEnabled() && !mSkipTest && (timeoutCount > 0) && + stream.getState() == StreamConfiguration.STREAM_STATE_STARTED) { + flushLog(); + Thread.sleep(POLL_DURATION_MILLIS); + timeoutCount--; + if (timeoutCount == 0) { + mTestFailed = true; + } else { + setStatusText("Plug detected by Java.\nCounting down to Oboe failure: " + timeoutCount); + } + } + + if (mSkipTest) { + setStatusText("Skipped"); + } else { + if (mTestFailed) { + // Check whether the peripheral has a microphone. + // Sometimes the microphones does not appear on the first HEADSET_PLUG event. + if (isInput && (mPlugMicrophone == 0)) { + hasMicFailed = true; + } + } else { + int error = stream.getLastErrorCallbackResult(); + if (error != StreamConfiguration.ERROR_DISCONNECTED) { + log("onErrorCallback error = " + error + + ", expected " + StreamConfiguration.ERROR_DISCONNECTED); + mTestFailed = true; + } + } + setStatusText(mTestFailed ? "Failed" : "Passed - detected"); + } + } + updateFailSkipButton(false); + setInstructionsText("Wait..."); + + if (!openFailed) { + stopAudioTest(); + } + + if (mSkipTest) valid = false; + + if (valid) { + if (openFailed) { + appendFailedSummary("------ #" + mAutomatedTestRunner.getTestCount() + "\n"); + appendFailedSummary(getConfigText(requestedConfig) + "\n"); + appendFailedSummary("Open failed!\n"); + mAutomatedTestRunner.incrementFailCount(); + } else { + log("Result:"); + boolean passed = !mTestFailed; + String resultText = requestPlugin ? "plugIN" : "UNplug"; + resultText += ", " + (passed ? TEXT_PASS : TEXT_FAIL); + if (hasMicFailed) { + resultText += ", Headset has no mic!"; + } + log(resultText); + if (!passed) { + appendFailedSummary("------ #" + mAutomatedTestRunner.getTestCount() + "\n"); + appendFailedSummary(" " + actualConfigText + "\n"); + appendFailedSummary(" " + resultText + "\n"); + mAutomatedTestRunner.incrementFailCount(); + } else { + mAutomatedTestRunner.incrementPassCount(); + } + } + } else { + log(TEXT_SKIP); + } + flushLog(); + // Give hardware time to settle between tests. + Thread.sleep(1000); + mAutomatedTestRunner.incrementTestCount(); + } + + private void testConfiguration(boolean isInput, int performanceMode, + int sharingMode, int sampleRate, + int sampleRateConversionQuality) throws InterruptedException { + boolean requestPlugin = true; // plug IN + testConfiguration(isInput, performanceMode, sharingMode, sampleRate, + sampleRateConversionQuality, requestPlugin); + requestPlugin = false; // UNplug + testConfiguration(isInput, performanceMode, sharingMode, sampleRate, + sampleRateConversionQuality, requestPlugin); + } + + private void testConfiguration(boolean isInput, int performanceMode, + int sharingMode, int sampleRate) throws InterruptedException { + testConfiguration(isInput, performanceMode, sharingMode, sampleRate, + StreamConfiguration.RATE_CONVERSION_QUALITY_NONE); + } + + private void testConfiguration(boolean isInput, int performanceMode, + int sharingMode) throws InterruptedException { + final int sampleRate = 0; + testConfiguration(isInput, performanceMode, sharingMode, sampleRate); + } + + private void testConfiguration(int performanceMode, + int sharingMode) throws InterruptedException { + testConfiguration(false, performanceMode, sharingMode); + testConfiguration(true, performanceMode, sharingMode); + } + + private void testConfiguration(int performanceMode, + int sharingMode, int sampleRate, + int sampleRateConversionQuality) throws InterruptedException { + testConfiguration(false, performanceMode, sharingMode, sampleRate, sampleRateConversionQuality); + testConfiguration(true, performanceMode, sharingMode, sampleRate, sampleRateConversionQuality); + } + + @Override + public void runTest() { + + runOnUiThread(() -> keepScreenOn(true)); + + mPlugCount = 0; + + // Try several different configurations. + try { + testConfiguration(StreamConfiguration.PERFORMANCE_MODE_NONE, + StreamConfiguration.SHARING_MODE_SHARED); + if (NativeEngine.isMMapExclusiveSupported()){ + testConfiguration(StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY, + StreamConfiguration.SHARING_MODE_EXCLUSIVE); + } + testConfiguration(StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY, + StreamConfiguration.SHARING_MODE_SHARED); + testConfiguration(StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY, + StreamConfiguration.SHARING_MODE_SHARED, 44100, + StreamConfiguration.RATE_CONVERSION_QUALITY_NONE); + testConfiguration(StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY, + StreamConfiguration.SHARING_MODE_SHARED, 44100, + StreamConfiguration.RATE_CONVERSION_QUALITY_MEDIUM); + } catch (InterruptedException e) { + log("Test CANCELLED - INVALID!"); + } catch (Exception e) { + log(e); + showErrorToast("Caught " + e); + } finally { + setInstructionsText("Test finished."); + updateFailSkipButton(false); + runOnUiThread(() -> keepScreenOn(false)); + } + } +} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestErrorCallbackActivity.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestErrorCallbackActivity.java new file mode 100644 index 00000000..d8545e38 --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestErrorCallbackActivity.java @@ -0,0 +1,101 @@ +/* + * 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. + */ + +package com.mobileer.oboetester; + +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.view.View; +import android.widget.TextView; + +import androidx.appcompat.app.AppCompatActivity; + +public class TestErrorCallbackActivity extends AppCompatActivity { + + private TextView mStatusDeleteCallback; + // This must match the value in TestErrorCallback.h + private static final int MAGIC_GOOD = 0x600DCAFE; + private MyStreamSniffer mStreamSniffer; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_error_callback); + + mStreamSniffer = new MyStreamSniffer(); + mStatusDeleteCallback = (TextView) findViewById(R.id.text_callback_status); + } + + public void onTestDeleteCrash(View view) { + mStatusDeleteCallback.setText(getString(R.string.plug_or_unplug)); + mStreamSniffer.startStreamSniffer(); + testDeleteCrash(); + } + + + // Periodically query the status of the streams. + protected class MyStreamSniffer { + public static final int SNIFFER_UPDATE_PERIOD_MSEC = 150; + public static final int SNIFFER_UPDATE_DELAY_MSEC = 300; + + private Handler mHandler; + + // Display status info for the stream. + private Runnable runnableCode = new Runnable() { + @Override + public void run() { + int magic = getCallbackMagic(); + updateMagicDisplay(magic); + mHandler.postDelayed(runnableCode, SNIFFER_UPDATE_PERIOD_MSEC); + } + }; + + private void startStreamSniffer() { + stopStreamSniffer(); + mHandler = new Handler(Looper.getMainLooper()); + // Start the initial runnable task by posting through the handler + mHandler.postDelayed(runnableCode, SNIFFER_UPDATE_DELAY_MSEC); + } + + private void stopStreamSniffer() { + if (mHandler != null) { + mHandler.removeCallbacks(runnableCode); + } + } + } + + private void updateMagicDisplay(int magic) { + if (magic != 0) { + String text = getString(R.string.report_magic_pass, MAGIC_GOOD); + if (magic != MAGIC_GOOD) { + text = getString(R.string.report_magic_fail, + magic, MAGIC_GOOD); + } + mStatusDeleteCallback.setText(text); + } + } + + @Override + public void onPause() { + super.onPause(); + mStreamSniffer.stopStreamSniffer(); + } + + private native void testDeleteCrash(); + private native int getCallbackMagic(); + +} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestInputActivity.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestInputActivity.java similarity index 65% rename from mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestInputActivity.java rename to mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestInputActivity.java index 6ad04808..f2d328e5 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestInputActivity.java +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestInputActivity.java @@ -14,26 +14,19 @@ * limitations under the License. */ -package com.google.sample.oboe.manualtest; +package com.mobileer.oboetester; -import android.Manifest; import android.content.Intent; -import android.content.pm.PackageManager; -import android.media.audiofx.AcousticEchoCanceler; -import android.media.audiofx.AutomaticGainControl; import android.net.Uri; import android.os.Bundle; import android.os.Environment; -import android.support.annotation.NonNull; -import android.support.v4.app.ActivityCompat; -import android.support.v4.content.FileProvider; +import android.os.Handler; +import android.os.Looper; import android.view.View; -import android.widget.CheckBox; import android.widget.RadioButton; -import android.widget.TextView; -import android.widget.Toast; -import com.google.sample.oboe.manualtest.R; +import androidx.annotation.NonNull; +import androidx.core.content.FileProvider; import java.io.File; import java.io.IOException; @@ -42,15 +35,14 @@ import java.io.IOException; * Test Oboe Capture */ -public class TestInputActivity extends TestAudioActivity - implements ActivityCompat.OnRequestPermissionsResultCallback { +public class TestInputActivity extends TestAudioActivity { - private static final int AUDIO_ECHO_REQUEST = 0; protected AudioInputTester mAudioInputTester; - private static final int NUM_VOLUME_BARS = 4; + // Note that this must match the number of volume bars defined in the layout file. + private static final int NUM_VOLUME_BARS = 8; private VolumeBarView[] mVolumeBars = new VolumeBarView[NUM_VOLUME_BARS]; private InputMarginView mInputMarginView; - private int mInputMarginBursts = 0; + int mInputMarginBursts = 0; private WorkloadView mWorkloadView; public native void setMinimumFramesBeforeRead(int frames); @@ -74,6 +66,10 @@ public class TestInputActivity extends TestAudioActivity mVolumeBars[1] = (VolumeBarView) findViewById(R.id.volumeBar1); mVolumeBars[2] = (VolumeBarView) findViewById(R.id.volumeBar2); mVolumeBars[3] = (VolumeBarView) findViewById(R.id.volumeBar3); + mVolumeBars[4] = (VolumeBarView) findViewById(R.id.volumeBar4); + mVolumeBars[5] = (VolumeBarView) findViewById(R.id.volumeBar5); + mVolumeBars[6] = (VolumeBarView) findViewById(R.id.volumeBar6); + mVolumeBars[7] = (VolumeBarView) findViewById(R.id.volumeBar7); mInputMarginView = (InputMarginView) findViewById(R.id.input_margin_view); @@ -83,14 +79,15 @@ public class TestInputActivity extends TestAudioActivity mWorkloadView = (WorkloadView) findViewById(R.id.workload_view); if (mWorkloadView != null) { - mWorkloadView.setAudioStreamTester(mAudioInputTester); + mWorkloadView.setWorkloadReceiver((w) -> mAudioInputTester.setWorkload(w)); } + + mCommunicationDeviceView = (CommunicationDeviceView) findViewById(R.id.comm_device_view); } @Override - protected void onStart() { - super.onStart(); - setActivityType(ACTIVITY_TEST_INPUT); + int getActivityType() { + return ACTIVITY_TEST_INPUT; } @Override @@ -108,14 +105,14 @@ public class TestInputActivity extends TestAudioActivity for (int i = 0; i < numChannels; i++) { if (mVolumeBars[i] == null) break; double level = mAudioInputTester.getPeakLevel(i); - mVolumeBars[i].setVolume((float) level); + mVolumeBars[i].setAmplitude((float) level); } } void resetVolumeBars() { for (int i = 0; i < mVolumeBars.length; i++) { if (mVolumeBars[i] == null) break; - mVolumeBars[i].setVolume((float) 0.0); + mVolumeBars[i].setAmplitude((float) 0.0); } } @@ -127,11 +124,16 @@ public class TestInputActivity extends TestAudioActivity } @Override - public void openAudio() throws IOException { - if (!isRecordPermissionGranted()){ - requestRecordPermission(); - return; + public void openAudio(View view) { + try { + openAudio(); + } catch (Exception e) { + showErrorToast(e.getMessage()); } + } + + @Override + public void openAudio() throws IOException { super.openAudio(); setMinimumBurstsBeforeRead(mInputMarginBursts); resetVolumeBars(); @@ -143,57 +145,6 @@ public class TestInputActivity extends TestAudioActivity resetVolumeBars(); } - private boolean isRecordPermissionGranted() { - return (ActivityCompat.checkSelfPermission(this, - Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED); - } - - private void requestRecordPermission(){ - ActivityCompat.requestPermissions( - this, - new String[]{Manifest.permission.RECORD_AUDIO}, - AUDIO_ECHO_REQUEST); - } - - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, - @NonNull int[] grantResults) { - - if (AUDIO_ECHO_REQUEST != requestCode) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - return; - } - - if (grantResults.length != 1 || - grantResults[0] != PackageManager.PERMISSION_GRANTED) { - - Toast.makeText(getApplicationContext(), - getString(R.string.need_record_audio_permission), - Toast.LENGTH_SHORT) - .show(); - } else { - // Permission was granted - try { - super.openAudio(); - } catch (IOException e) { - showErrorToast(e.getMessage()); - } - } - } - - public void setupAGC(int sessionId) { - AutomaticGainControl effect = AutomaticGainControl.create(sessionId); - } - - public void setupAEC(int sessionId) { - AcousticEchoCanceler effect = AcousticEchoCanceler.create(sessionId); - } - - @Override - public void setupEffects(int sessionId) { - setupAEC(sessionId); - } - protected int saveWaveFile(File file) { // Pass filename to native to write WAV file int result = saveWaveFile(file.getAbsolutePath()); @@ -213,7 +164,7 @@ public class TestInputActivity extends TestAudioActivity private File createFileName() { // Get directory and filename File dir = getExternalFilesDir(Environment.DIRECTORY_MUSIC); - return new File(dir, "oboe_" + getWaveTag() + "_" + getTimestampString() + ".wav"); + return new File(dir, "oboe_" + getWaveTag() + "_" + AutomatedTestRunner.getTimestampString() + ".wav"); } public void shareWaveFile() { @@ -244,4 +195,38 @@ public class TestInputActivity extends TestAudioActivity mInputMarginBursts = Integer.parseInt(text); setMinimumBurstsBeforeRead(mInputMarginBursts); } + + @Override + public void startTestUsingBundle() { + try { + StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration; + IntentBasedTestSupport.configureInputStreamFromBundle(mBundleFromIntent, requestedInConfig); + + openAudio(); + startAudio(); + + int durationSeconds = IntentBasedTestSupport.getDurationSeconds(mBundleFromIntent); + if (durationSeconds > 0) { + // Schedule the end of the test. + Handler handler = new Handler(Looper.getMainLooper()); // UI thread + handler.postDelayed(new Runnable() { + @Override + public void run() { + stopAutomaticTest(); + } + }, durationSeconds * 1000); + } + } catch (Exception e) { + showErrorToast(e.getMessage()); + } finally { + mBundleFromIntent = null; + } + } + + void stopAutomaticTest() { + String report = getCommonTestReport(); + stopAudio(); + maybeWriteTestResult(report); + mTestRunningByIntent = false; + } } diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestOutputActivity.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestOutputActivity.java new file mode 100644 index 00000000..f2ac5ed5 --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestOutputActivity.java @@ -0,0 +1,228 @@ +/* + * Copyright 2017 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. + */ + +package com.mobileer.oboetester; + +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.view.View; +import android.widget.AdapterView; +import android.widget.CheckBox; +import android.widget.SeekBar; +import android.widget.Spinner; +import android.widget.TextView; + +import java.io.IOException; +import java.util.Locale; + +/** + * Test basic output. + */ +public final class TestOutputActivity extends TestOutputActivityBase { + + public static final int MAX_CHANNEL_BOXES = 16; + private CheckBox[] mChannelBoxes; + private Spinner mOutputSignalSpinner; + private TextView mVolumeTextView; + private SeekBar mVolumeSeekBar; + private CheckBox mShouldSetStreamControlByAttributes; + + private class OutputSignalSpinnerListener implements android.widget.AdapterView.OnItemSelectedListener { + @Override + public void onItemSelected(AdapterView parent, View view, int pos, long id) { + mAudioOutTester.setSignalType(pos); + } + + @Override + public void onNothingSelected(AdapterView parent) { + mAudioOutTester.setSignalType(0); + } + } + + private SeekBar.OnSeekBarChangeListener mVolumeChangeListener = new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + setVolume(progress); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + } + }; + + @Override + protected void inflateActivity() { + setContentView(R.layout.activity_test_output); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + updateEnabledWidgets(); + + mAudioOutTester = addAudioOutputTester(); + + mChannelBoxes = new CheckBox[MAX_CHANNEL_BOXES]; + int ic = 0; + mChannelBoxes[ic++] = (CheckBox) findViewById(R.id.channelBox0); + mChannelBoxes[ic++] = (CheckBox) findViewById(R.id.channelBox1); + mChannelBoxes[ic++] = (CheckBox) findViewById(R.id.channelBox2); + mChannelBoxes[ic++] = (CheckBox) findViewById(R.id.channelBox3); + mChannelBoxes[ic++] = (CheckBox) findViewById(R.id.channelBox4); + mChannelBoxes[ic++] = (CheckBox) findViewById(R.id.channelBox5); + mChannelBoxes[ic++] = (CheckBox) findViewById(R.id.channelBox6); + mChannelBoxes[ic++] = (CheckBox) findViewById(R.id.channelBox7); + mChannelBoxes[ic++] = (CheckBox) findViewById(R.id.channelBox8); + mChannelBoxes[ic++] = (CheckBox) findViewById(R.id.channelBox9); + mChannelBoxes[ic++] = (CheckBox) findViewById(R.id.channelBox10); + mChannelBoxes[ic++] = (CheckBox) findViewById(R.id.channelBox11); + mChannelBoxes[ic++] = (CheckBox) findViewById(R.id.channelBox12); + mChannelBoxes[ic++] = (CheckBox) findViewById(R.id.channelBox13); + mChannelBoxes[ic++] = (CheckBox) findViewById(R.id.channelBox14); + mChannelBoxes[ic++] = (CheckBox) findViewById(R.id.channelBox15); + configureChannelBoxes(0); + + mOutputSignalSpinner = (Spinner) findViewById(R.id.spinnerOutputSignal); + mOutputSignalSpinner.setOnItemSelectedListener(new OutputSignalSpinnerListener()); + mOutputSignalSpinner.setSelection(StreamConfiguration.NATIVE_API_UNSPECIFIED); + + mCommunicationDeviceView = (CommunicationDeviceView) findViewById(R.id.comm_device_view); + + mVolumeTextView = (TextView) findViewById(R.id.textVolumeSlider); + mVolumeSeekBar = (SeekBar) findViewById(R.id.faderVolumeSlider); + mVolumeSeekBar.setOnSeekBarChangeListener(mVolumeChangeListener); + + mShouldSetStreamControlByAttributes = (CheckBox) findViewById(R.id.enableSetStreamControlByAttributes); + } + + @Override + int getActivityType() { + return ACTIVITY_TEST_OUTPUT; + } + + public void openAudio() throws IOException { + super.openAudio(); + mShouldSetStreamControlByAttributes.setEnabled(false); + } + + private void configureChannelBoxes(int channelCount) { + for (int i = 0; i < mChannelBoxes.length; i++) { + mChannelBoxes[i].setChecked(i < channelCount); + mChannelBoxes[i].setEnabled(i < channelCount); + } + } + + private void setVolume(int progress) { + // Convert from (0, 500) range to (-50, 0). + double decibels = (progress - 500) / 10.0f; + double amplitude = Math.pow(10.0, decibels / 20.0); + // When the slider is all way to the left, set a zero amplitude. + if (progress == 0) { + amplitude = 0; + } + mVolumeTextView.setText("Volume(dB): " + String.format(Locale.getDefault(), "%.1f", + decibels)); + mAudioOutTester.setAmplitude((float) amplitude); + } + + + public void stopAudio() { + configureChannelBoxes(0); + mOutputSignalSpinner.setEnabled(true); + super.stopAudio(); + } + + public void pauseAudio() { + configureChannelBoxes(0); + mOutputSignalSpinner.setEnabled(true); + super.pauseAudio(); + } + + public void releaseAudio() { + configureChannelBoxes(0); + mOutputSignalSpinner.setEnabled(true); + super.releaseAudio(); + } + + public void closeAudio() { + configureChannelBoxes(0); + mOutputSignalSpinner.setEnabled(true); + mShouldSetStreamControlByAttributes.setEnabled(true); + super.closeAudio(); + } + + public void startAudio() throws IOException { + super.startAudio(); + int channelCount = mAudioOutTester.getCurrentAudioStream().getChannelCount(); + configureChannelBoxes(channelCount); + mOutputSignalSpinner.setEnabled(false); + } + + public void onChannelBoxClicked(View view) { + CheckBox checkBox = (CheckBox) view; + String text = (String) checkBox.getText(); + int channelIndex = Integer.parseInt(text); + mAudioOutTester.setChannelEnabled(channelIndex, checkBox.isChecked()); + } + + @Override + protected boolean shouldSetStreamControlByAttributes() { + return mShouldSetStreamControlByAttributes.isChecked(); + } + + @Override + public void startTestUsingBundle() { + try { + StreamConfiguration requestedOutConfig = mAudioOutTester.requestedConfiguration; + IntentBasedTestSupport.configureOutputStreamFromBundle(mBundleFromIntent, requestedOutConfig); + + int signalType = IntentBasedTestSupport.getSignalTypeFromBundle(mBundleFromIntent); + mAudioOutTester.setSignalType(signalType); + + openAudio(); + startAudio(); + + int durationSeconds = IntentBasedTestSupport.getDurationSeconds(mBundleFromIntent); + if (durationSeconds > 0) { + // Schedule the end of the test. + Handler handler = new Handler(Looper.getMainLooper()); // UI thread + handler.postDelayed(new Runnable() { + @Override + public void run() { + stopAutomaticTest(); + } + }, durationSeconds * 1000); + } + } catch (Exception e) { + showErrorToast(e.getMessage()); + } finally { + mBundleFromIntent = null; + } + } + + void stopAutomaticTest() { + String report = getCommonTestReport(); + stopAudio(); + maybeWriteTestResult(report); + mTestRunningByIntent = false; + } +} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestOutputActivityBase.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestOutputActivityBase.java similarity index 50% rename from mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestOutputActivityBase.java rename to mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestOutputActivityBase.java index 9ea9164f..c7420456 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestOutputActivityBase.java +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestOutputActivityBase.java @@ -14,22 +14,15 @@ * limitations under the License. */ -package com.google.sample.oboe.manualtest; - -import android.media.audiofx.Equalizer; -import android.media.audiofx.PresetReverb; -import android.util.Log; -import android.widget.SeekBar; -import android.widget.TextView; +package com.mobileer.oboetester; import java.io.IOException; - abstract class TestOutputActivityBase extends TestAudioActivity { AudioOutputTester mAudioOutTester; private BufferSizeView mBufferSizeView; - private WorkloadView mWorkloadView; + protected WorkloadView mWorkloadView; @Override boolean isOutput() { return true; } @@ -43,48 +36,21 @@ abstract class TestOutputActivityBase extends TestAudioActivity { super.findAudioCommon(); mBufferSizeView = (BufferSizeView) findViewById(R.id.buffer_size_view); mWorkloadView = (WorkloadView) findViewById(R.id.workload_view); + if (mWorkloadView != null) { + mWorkloadView.setWorkloadReceiver((w) -> mAudioOutTester.setWorkload(w)); + } } @Override public AudioOutputTester addAudioOutputTester() { - AudioOutputTester audioOutTester = super.addAudioOutputTester(); - mBufferSizeView.setAudioOutTester(audioOutTester); - mWorkloadView.setAudioStreamTester(audioOutTester); - return audioOutTester; + return super.addAudioOutputTester(); } - @Override public void openAudio() throws IOException { super.openAudio(); if (mBufferSizeView != null) { - mBufferSizeView.updateBufferSize(); + mBufferSizeView.onStreamOpened((OboeAudioStream) mAudioOutTester.getCurrentAudioStream()); } } - - // TODO Add editor - public void setupEqualizer(int sessionId) { - Equalizer equalizer = new Equalizer(0, sessionId); - int numBands = equalizer.getNumberOfBands(); - Log.d(TAG, "numBands " + numBands); - for (short band = 0; band < numBands; band++) { - String msg = "band " + band - + ", center = " + equalizer.getCenterFreq(band) - + ", level = " + equalizer.getBandLevel(band); - Log.d(TAG, msg); - equalizer.setBandLevel(band, (short)40); - } - - equalizer.setBandLevel((short) 1, (short) 300); - } - - public void setupReverb(int sessionId) { - PresetReverb effect = new PresetReverb(0, sessionId); - } - - @Override - public void setupEffects(int sessionId) { - // setupEqualizer(sessionId); - // setupReverb(sessionId); - } -} \ No newline at end of file +} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestPlugLatencyActivity.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestPlugLatencyActivity.java new file mode 100644 index 00000000..5f23cae0 --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestPlugLatencyActivity.java @@ -0,0 +1,286 @@ +/* + * 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. + */ + +package com.mobileer.oboetester; + +import android.annotation.TargetApi; +import android.content.Context; +import android.media.AudioDeviceCallback; +import android.media.AudioDeviceInfo; +import android.media.AudioManager; +import android.os.Bundle; +import android.widget.TextView; + +import com.mobileer.audio_device.AudioDeviceInfoConverter; + +import java.io.IOException; +import java.util.HashMap; + +/** + * Tests the latency of plugging in or unplugging an audio device. + */ +public class TestPlugLatencyActivity extends TestAudioActivity { + + public static final int POLL_DURATION_MILLIS = 1; + public static final int TIMEOUT_MILLIS = 1000; + + private TextView mInstructionsTextView; + private TextView mPlugTextView; + private TextView mAutoTextView; + MyAudioDeviceCallback mDeviceCallback = new MyAudioDeviceCallback(); + private AudioManager mAudioManager; + + private volatile int mPlugCount = 0; + private long mTimeoutAtMillis; + + private AudioOutputTester mAudioOutTester; + + class MyAudioDeviceCallback extends AudioDeviceCallback { + private HashMap mDevices + = new HashMap(); + + @Override + public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) { + boolean isBootingUp = mDevices.isEmpty(); + AudioDeviceInfo outputDeviceInfo = null; + for (AudioDeviceInfo info : addedDevices) { + mDevices.put(info.getId(), info); + if (!isBootingUp) + { + log("====== Device Added ======="); + log(adiToString(info)); + // Only process OUTPUT devices because that is what we are testing. + if (info.isSink()) { + outputDeviceInfo = info; + } + } + + } + + if (isBootingUp) { + log("Starting stream with existing audio devices"); + } + if (outputDeviceInfo != null) { + updateLatency(false /* wasDeviceRemoved */); + } + } + + public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) { + AudioDeviceInfo outputDeviceInfo = null; + for (AudioDeviceInfo info : removedDevices) { + mDevices.remove(info.getId()); + log("====== Device Removed ======="); + log(adiToString(info)); + // Only process OUTPUT devices because that is what we are testing. + if (info.isSink()) { + outputDeviceInfo = info; + } + } + + if (outputDeviceInfo != null) { + updateLatency(true /* wasDeviceRemoved */); + } + } + } + + @Override + protected void inflateActivity() { + setContentView(R.layout.activity_test_plug_latency); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mInstructionsTextView = (TextView) findViewById(R.id.text_instructions); + mPlugTextView = (TextView) findViewById(R.id.text_plug_events); + mAutoTextView = (TextView) findViewById(R.id.text_log_device_report); + + mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + } + + @Override + protected void onStart() { + super.onStart(); + addAudioDeviceCallback(); + } + + @Override + protected void onStop() { + removeAudioDeviceCallback(); + super.onStop(); + } + + @TargetApi(23) + private void addAudioDeviceCallback(){ + // Note that we will immediately receive a call to onDevicesAdded with the list of + // devices which are currently connected. + mAudioManager.registerAudioDeviceCallback(mDeviceCallback, null); + } + + @TargetApi(23) + private void removeAudioDeviceCallback(){ + mAudioManager.unregisterAudioDeviceCallback(mDeviceCallback); + } + + @Override + public String getTestName() { + return "Plug Latency"; + } + + int getActivityType() { + return ACTIVITY_TEST_DISCONNECT; + } + + @Override + boolean isOutput() { + return true; + } + + // Write to status and command view + private void setInstructionsText(final String text) { + runOnUiThread(new Runnable() { + @Override + public void run() { + mInstructionsTextView.setText(text); + } + }); + } + + public void startAudioTest() throws IOException { + startAudio(); + } + + private void setupTimeout() { + mTimeoutAtMillis = System.currentTimeMillis() + TIMEOUT_MILLIS; + } + + private void sleepOrTimeout(String message) throws InterruptedException { + Thread.sleep(POLL_DURATION_MILLIS); + if (System.currentTimeMillis() >= mTimeoutAtMillis) { + throw new InterruptedException(message); + } + } + + private long calculateLatencyMs(boolean wasDeviceRemoved) { + long testStartMillis = System.currentTimeMillis(); + long frameReadMillis = -1; + final int TIMEOUT_MAX = 100; + int timeout; + try { + long callbackMillis = -1; + if (wasDeviceRemoved && (mAudioOutTester != null)) { + log("Wait for error callback != 0"); + // Keep querying as long as error is ok + setupTimeout(); + while (mAudioOutTester.getLastErrorCallbackResult() == 0) { + sleepOrTimeout("timed out waiting while error==0"); + } + callbackMillis = System.currentTimeMillis(); + log("Error callback at " + (callbackMillis - testStartMillis) + " ms. " + + "WAIT -> CALLBACK = took " + (callbackMillis - testStartMillis) + " ms"); + } + closeAudio(); + long closedMillis = System.currentTimeMillis(); + if (callbackMillis == -1) { + log("Audio closed at " + (closedMillis - testStartMillis) + " ms"); + } else { + log("Audio closed at " + (closedMillis - testStartMillis) + " ms. " + + "CALLBACK -> CLOSED took " + (closedMillis - callbackMillis) + " ms"); + } + + clearStreamContexts(); + mAudioOutTester = addAudioOutputTester(); + openAudio(); + long openedMillis = System.currentTimeMillis(); + log("Audio opened at " + (openedMillis - testStartMillis) + " ms. " + + "CLOSED -> OPENED took " + (openedMillis - closedMillis) + " ms"); + AudioStreamBase stream = mAudioOutTester.getCurrentAudioStream(); + startAudioTest(); + long startingMillis = System.currentTimeMillis(); + log("Audio starting at " + (startingMillis - testStartMillis) + " ms. " + + "OPENED -> STARTING took " + (startingMillis - openedMillis) + " ms"); + + setupTimeout(); + while (stream.getState() == StreamConfiguration.STREAM_STATE_STARTING) { + sleepOrTimeout("timed out waiting while STATE_STARTING"); + } + long startedMillis = System.currentTimeMillis(); + log("Audio started at " + (startedMillis - testStartMillis) + " ms. " + + "STARTING -> STARTED took " + (startedMillis - startingMillis) + " ms"); + + setupTimeout(); + while (mAudioOutTester.getFramesRead() == 0) { + sleepOrTimeout("timed out waiting while framesRead()==0"); + } + frameReadMillis = System.currentTimeMillis(); + log("First frame read at " + (frameReadMillis - testStartMillis) + " ms. " + + "STARTED -> READ took " + (frameReadMillis - startedMillis) + " ms"); + } catch (IOException | InterruptedException e) { + log("EXCEPTION: " + e); + e.printStackTrace(); + closeAudio(); + return -1; + } + + return frameReadMillis - testStartMillis; + } + + public static String adiToString(AudioDeviceInfo adi) { + + StringBuilder sb = new StringBuilder(); + sb.append("Id: "); + sb.append(adi.getId()); + + sb.append("\nProduct name: "); + sb.append(adi.getProductName()); + + sb.append("\nType: "); + sb.append(AudioDeviceInfoConverter.typeToString(adi.getType())); + + sb.append("\nIsSource: "); + sb.append(String.valueOf(adi.isSource())); + sb.append(", IsSink: "); + sb.append(String.valueOf(adi.isSink())); + + return sb.toString(); + } + + // Write to scrollable TextView + private void log(final String text) { + runOnUiThread(new Runnable() { + @Override + public void run() { + mAutoTextView.append(text); + mAutoTextView.append("\n"); + } + }); + } + + private void updateLatency(boolean wasDeviceRemoved) { + mPlugCount++; + log("\nOperation #" + mPlugCount + " starting"); + long latencyMs = calculateLatencyMs(wasDeviceRemoved); + String message = "Operation #" + mPlugCount + " latency: "+ latencyMs + " ms\n"; + log(message); + runOnUiThread(new Runnable() { + @Override + public void run() { + mPlugTextView.setText(message); + } + }); + } +} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestRapidCycleActivity.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestRapidCycleActivity.java new file mode 100644 index 00000000..a265e359 --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestRapidCycleActivity.java @@ -0,0 +1,155 @@ +/* + * 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. + */ + +package com.mobileer.oboetester; + +import static com.mobileer.oboetester.TestAudioActivity.TAG; + +import android.content.Context; +import android.media.AudioManager; +import android.os.Bundle; +import android.util.Log; +import android.view.View; +import android.view.WindowManager; +import android.widget.Button; +import android.widget.RadioButton; +import android.widget.TextView; + +import androidx.appcompat.app.AppCompatActivity; + +/** + * Try to hang streams by rapidly opening and closing. + * See b/348615156 + */ +public class TestRapidCycleActivity extends AppCompatActivity { + + private TextView mStatusView; + private MyStreamSniffer mStreamSniffer; + private AudioManager mAudioManager; + private RadioButton mApiOpenSLButton; + private RadioButton mApiAAudioButton; + private Button mStartButton; + private Button mStopButton; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_rapid_cycle); + mStatusView = (TextView) findViewById(R.id.text_callback_status); + mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + + mStartButton = (Button) findViewById(R.id.button_start_test); + mStopButton = (Button) findViewById(R.id.button_stop_test); + mApiOpenSLButton = (RadioButton) findViewById(R.id.audio_api_opensl); + mApiOpenSLButton.setChecked(true); + mApiAAudioButton = (RadioButton) findViewById(R.id.audio_api_aaudio); + setButtonsEnabled(false); + } + + public void onStartCycleTest(View view) { startCycleTest(); } + public void onStopCycleTest(View view) { + stopCycleTest(); + } + + private void setButtonsEnabled(boolean running) { + mStartButton.setEnabled(!running); + mStopButton.setEnabled(running); + mApiOpenSLButton.setEnabled(!running); + mApiAAudioButton.setEnabled(!running); + } + + // Change routing while the stream is playing. + // Keep trying until we crash. + protected class MyStreamSniffer extends Thread { + boolean enabled = true; + StringBuffer statusBuffer = new StringBuffer(); + + @Override + public void run() { + int lastCycleCount = -1; + boolean useOpenSL = mApiOpenSLButton.isChecked(); + startRapidCycleTest(useOpenSL); + try { + while (enabled) { + statusBuffer = new StringBuffer(); + sleep(100); + int cycleCount = getCycleCount(); + if (cycleCount > lastCycleCount) { // reduce spam + log("#" + cycleCount + " open/close cycles\n"); + lastCycleCount = cycleCount; + } + } + } catch (InterruptedException e) { + } finally { + stopRapidCycleTest(); + } + } + + // Log to screen and logcat. + private void log(String text) { + Log.d(TAG, "RapidCycle: " + text); + statusBuffer.append(text); + showStatus(statusBuffer.toString()); + } + + // Stop the test thread. + void finish() { + enabled = false; + interrupt(); + try { + join(2000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + protected void showStatus(final String message) { + runOnUiThread(new Runnable() { + @Override + public void run() { + mStatusView.setText(message); + } + }); + } + + private native int startRapidCycleTest(boolean useOpenSL); + private native int stopRapidCycleTest(); + private native int getCycleCount(); + + @Override + public void onPause() { + super.onPause(); + Log.i(TAG, "onPause() called so stop the test ========================="); + stopCycleTest(); + } + + private void startCycleTest() { + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + setButtonsEnabled(true); + mStreamSniffer = new MyStreamSniffer(); + mStreamSniffer.start(); + } + + private void stopCycleTest() { + if (mStreamSniffer != null) { + mStreamSniffer.finish(); + mStreamSniffer = null; + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + setButtonsEnabled(false); + } + } +} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestRouteDuringCallbackActivity.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestRouteDuringCallbackActivity.java new file mode 100644 index 00000000..0e43f37e --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestRouteDuringCallbackActivity.java @@ -0,0 +1,178 @@ +/* + * 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. + */ + +package com.mobileer.oboetester; + +import static com.mobileer.oboetester.TestAudioActivity.TAG; + +import android.content.Context; +import android.media.AudioManager; +import android.os.Bundle; +import android.util.Log; +import android.view.View; +import android.view.WindowManager; +import android.widget.Button; +import android.widget.RadioButton; +import android.widget.TextView; + +import androidx.appcompat.app.AppCompatActivity; + +import java.util.Random; + +/** + * Try to crash in the native AAudio code by causing a routing change + * while playing audio. The buffer may get deleted while we are writing to it! + * See b/274815060 + */ +public class TestRouteDuringCallbackActivity extends AppCompatActivity { + + private TextView mStatusView; + private MyStreamSniffer mStreamSniffer; + private AudioManager mAudioManager; + private RadioButton mOutputButton; + private RadioButton mInputButton; + private Button mStartButton; + private Button mStopButton; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_routing_crash); + mStatusView = (TextView) findViewById(R.id.text_callback_status); + mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + + mStartButton = (Button) findViewById(R.id.button_start_test); + mStopButton = (Button) findViewById(R.id.button_stop_test); + mOutputButton = (RadioButton) findViewById(R.id.direction_output); + mInputButton = (RadioButton) findViewById(R.id.direction_input); + setButtonsEnabled(false); + } + + public void onStartRoutingTest(View view) { + startRoutingTest(); + } + + public void onStopRoutingTest(View view) { + stopRoutingTest(); + } + + private void setButtonsEnabled(boolean running) { + mStartButton.setEnabled(!running); + mStopButton.setEnabled(running); + mOutputButton.setEnabled(!running); + mInputButton.setEnabled(!running); + } + + // Change routing while the stream is playing. + // Keep trying until we crash. + protected class MyStreamSniffer extends Thread { + boolean enabled = true; + int routingOption = 0; + StringBuffer statusBuffer = new StringBuffer(); + int loopCount; + + @Override + public void run() { + routingOption = 0; + changeRoute(routingOption); + int result; + Random random = new Random(); + while (enabled) { + loopCount++; + if (routingOption == 0) { + statusBuffer = new StringBuffer(); + } + try { + sleep(100); + boolean useInput = mInputButton.isChecked(); + result = startStream(useInput); + sleep(100); + log("-------#" + loopCount + ", " + (useInput ? "IN" : "OUT") + + "\nstartStream() returned " + result); + int sleepTimeMillis = 500 + random.nextInt(500); + sleep(sleepTimeMillis); + routingOption = (routingOption == 0) ? 1 : 0; + log( "changeRoute " + routingOption); + changeRoute(routingOption); + sleep(50); + } catch (InterruptedException e) { + } finally { + result = stopStream(); + log("stopStream() returned " + result); + } + } + changeRoute(0); + } + + // Log to screen and logcat. + private void log(String text) { + Log.d(TAG, "RoutingCrash: " + text); + statusBuffer.append(text + ", sleep " + getSleepTimeMicros() + " us\n"); + showStatus(statusBuffer.toString()); + } + + // Stop the test thread. + void finish() { + enabled = false; + interrupt(); + try { + join(2000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + protected void showStatus(final String message) { + runOnUiThread(new Runnable() { + @Override + public void run() { + mStatusView.setText(message); + } + }); + } + + private void changeRoute(int option) { + mAudioManager.setSpeakerphoneOn(option > 0); + } + + private native int startStream(boolean useInput); + private native int getSleepTimeMicros(); + private native int stopStream(); + + @Override + public void onPause() { + super.onPause(); + stopRoutingTest(); + } + + private void startRoutingTest() { + stopRoutingTest(); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + setButtonsEnabled(true); + mStreamSniffer = new MyStreamSniffer(); + mStreamSniffer.start(); + } + + private void stopRoutingTest() { + if (mStreamSniffer != null) { + mStreamSniffer.finish(); + mStreamSniffer = null; + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + setButtonsEnabled(false); + } + } +} diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/VolumeBarView.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/VolumeBarView.java similarity index 54% rename from mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/VolumeBarView.java rename to mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/VolumeBarView.java index a6f562fa..0dc6f4df 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/VolumeBarView.java +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/VolumeBarView.java @@ -14,24 +14,38 @@ * limitations under the License. */ -package com.google.sample.oboe.manualtest; +package com.mobileer.oboetester; import android.content.Context; -import android.content.res.Resources; -import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; +import android.graphics.Rect; import android.util.AttributeSet; import android.view.View; +import java.util.Locale; + +/** + * Display volume in dB as a red bar. + * Display the amplitude, 0.0 to 1.0, as text in the bar. + */ public class VolumeBarView extends View { + private static final float MIN_VOLUME_DB = -84; + private static final float MIN_AMPLITUDE_AT_MIN_VOLUME_DB = + (float) Math.pow(10.0, MIN_VOLUME_DB / 20); + public static final String FORMAT_AMPLITUDE = "%8.6f"; + + private float mClippedAmplitude; + private float mVolume = MIN_VOLUME_DB; + private Paint mBarPaint; + private Paint mTextPaint; + private Paint mBackgroundPaint; private int mCurrentWidth; private int mCurrentHeight; - private Paint mBackgroundPaint; - private float mVolume; + private final Rect mTextBounds = new Rect(); public VolumeBarView(Context context, AttributeSet attrs) { super(context, attrs); @@ -42,18 +56,23 @@ public class VolumeBarView extends View { private void init() { mBarPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - mBarPaint.setColor(Color.RED); + mBarPaint.setColor(Color.rgb(240, 100, 100)); mBarPaint.setStyle(Paint.Style.FILL); mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mBackgroundPaint.setColor(Color.LTGRAY); mBackgroundPaint.setStyle(Paint.Style.FILL); + + mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mTextPaint.setColor(Color.BLACK); + mTextPaint.setStyle(Paint.Style.FILL); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { mCurrentWidth = w; mCurrentHeight = h; + mTextPaint.setTextSize(0.8f * h); } @Override @@ -61,16 +80,28 @@ public class VolumeBarView extends View { super.onDraw(canvas); canvas.drawRect(0.0f, 0.0f, mCurrentWidth, mCurrentHeight, mBackgroundPaint); - float scaledVolume = mVolume * mCurrentWidth; - canvas.drawRect(0.0f, 0.0f, scaledVolume, + float volumeWidth = ((MIN_VOLUME_DB - mVolume) / MIN_VOLUME_DB) * mCurrentWidth; + canvas.drawRect(0.0f, 0.0f, volumeWidth, mCurrentHeight, mBarPaint); + String text = String.format(Locale.getDefault(), FORMAT_AMPLITUDE, mClippedAmplitude); + mTextPaint.getTextBounds(text, 0, text.length(), mTextBounds); + canvas.drawText(text, + 20.0f, + (mCurrentHeight/2) - mTextBounds.exactCenterY(), + mTextPaint); } /** - * Set volume between 0.0 and 1.0 + * Set peak amplitude of the signal. + * Should be between 0.0 and 1.0 */ - public void setVolume(float volume) { - mVolume = volume; + public void setAmplitude(float amplitude) { + mClippedAmplitude = (float) Math.min(1.0, Math.max(0.0, amplitude)); + if (amplitude < MIN_AMPLITUDE_AT_MIN_VOLUME_DB) { + mVolume = MIN_VOLUME_DB; + } else { + mVolume = 20.0f * (float)Math.log10(amplitude); + } postInvalidate(); } diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/WaveformView.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/WaveformView.java similarity index 78% rename from mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/WaveformView.java rename to mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/WaveformView.java index 580f4c9a..f271f6aa 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/WaveformView.java +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/WaveformView.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.sample.oboe.manualtest; +package com.mobileer.oboetester; import android.content.Context; import android.content.res.Resources; @@ -25,22 +25,24 @@ import android.graphics.Paint; import android.util.AttributeSet; import android.view.View; -import com.google.sample.oboe.manualtest.R; - /** * Display an audio waveform in a custom View. + * Data is assumed to have a constant x-axis increment, ie. isochronous. */ public class WaveformView extends View { + private static final float MESSAGE_TEXT_SIZE = 80; private Paint mWavePaint; + private Paint mBackgroundPaint; + private Paint mMessagePaint; private int mCurrentWidth; private int mCurrentHeight; - private Paint mBackgroundPaint; private float[] mData; private int mSampleCount; private int mSampleOffset; private float mOffsetY; private float mScaleY; private int[] mCursors; + private String mMessage; private Paint mCursorPaint; public WaveformView(Context context, AttributeSet attrs) { @@ -65,14 +67,32 @@ public class WaveformView extends View { mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mBackgroundPaint.setColor(res.getColor(R.color.waveform_background)); mBackgroundPaint.setStyle(Paint.Style.FILL); + + mMessagePaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mMessagePaint.setColor(res.getColor(R.color.waveform_line)); + mMessagePaint.setTextSize(MESSAGE_TEXT_SIZE); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { mCurrentWidth = w; mCurrentHeight = h; - mOffsetY = 0.5f * h; - mScaleY = 0.0f - mOffsetY; + mOffsetY = 0.5f * h; // Center waveform vertically in the viewport. + // Scale down so that we can see the top of the waveforms if they are clipped. + mScaleY = -0.95f * mOffsetY; // Negate so positive values are on top. + } + + public String getMessage() { + return mMessage; + } + + /** + * Set message to be displayed over the waveform. + * Set null for no message. + * @param message text or null + */ + public void setMessage(String message) { + this.mMessage = message; } @Override @@ -81,15 +101,29 @@ public class WaveformView extends View { float [] localData = mData; canvas.drawRect(0.0f, 0.0f, mCurrentWidth, mCurrentHeight, mBackgroundPaint); + if (mMessage != null) { + canvas.drawText(mMessage, 20, mCurrentHeight - 20, mMessagePaint); + } if (localData == null || mSampleCount == 0) { return; } + float xScale = ((float) mCurrentWidth) / (mSampleCount - 1); + + // Draw cursors. + if (mCursors != null) { + for (int i = 0; i < mCursors.length; i++) { + float x = mCursors[i] * xScale; + canvas.drawLine(x, 0, x, mCurrentHeight, mCursorPaint); + } + } + + // Draw waveform. float x0 = 0.0f; if (xScale < 1.0) { // Draw a vertical bar for multiple samples. - float ymin = mOffsetY; - float ymax = mOffsetY; + float ymin = mOffsetY; // vertical center + float ymax = mOffsetY; // vertical center for (int i = 0; i < mSampleCount; i++) { float x1 = i * xScale; if ((int) x0 != (int) x1) { @@ -114,16 +148,11 @@ public class WaveformView extends View { y0 = y1; } } - if (mCursors != null) { - for (int i = 0; i < mCursors.length; i++) { - float x = mCursors[i] * xScale; - canvas.drawLine(x, 0, x, mCurrentHeight, mCursorPaint); - } - } } /** - * Copy data into internal buffer then repaint. + * Copy data into internal buffer. + * Caller should then postInvalidate(). */ public void setSampleData(float[] samples) { setSampleData(samples, 0, samples.length); @@ -149,7 +178,8 @@ public class WaveformView extends View { } /** - * Copy cursor positions into internal buffer then repaint. + * Copy cursor positions into internal buffer. + * Caller should then postInvalidate(). */ public void setCursorData(int[] cursors) { if (cursors == null) { diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/WorkloadView.java b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/WorkloadView.java similarity index 77% rename from mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/WorkloadView.java rename to mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/WorkloadView.java index fcba2b6e..87d3d1df 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/WorkloadView.java +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/WorkloadView.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.sample.oboe.manualtest; +package com.mobileer.oboetester; import android.content.Context; import android.util.AttributeSet; @@ -23,15 +23,23 @@ import android.widget.LinearLayout; import android.widget.SeekBar; import android.widget.TextView; -public class WorkloadView extends LinearLayout { +import java.util.Locale; - private AudioStreamTester mAudioStreamTester; +public class WorkloadView extends LinearLayout { protected static final int FADER_PROGRESS_MAX = 1000; // must match layout protected TextView mTextView; protected SeekBar mSeekBar; + + private String mLabel = "Workload"; protected ExponentialTaper mExponentialTaper; + public interface WorkloadReceiver { + void setWorkload(int workload); + } + + WorkloadReceiver mWorkloadReceiver; + private SeekBar.OnSeekBarChangeListener mChangeListener = new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { @@ -64,12 +72,8 @@ public class WorkloadView extends LinearLayout { initializeViews(context); } - public AudioStreamTester getAudioStreamTester() { - return mAudioStreamTester; - } - - public void setAudioStreamTester(AudioStreamTester audioStreamTester) { - mAudioStreamTester = audioStreamTester; + public void setWorkloadReceiver(WorkloadReceiver workloadReceiver) { + mWorkloadReceiver = workloadReceiver; } void setFaderNormalizedProgress(double fraction) { @@ -90,20 +94,21 @@ public class WorkloadView extends LinearLayout { mTextView = (TextView) findViewById(R.id.textWorkload); mSeekBar = (SeekBar) findViewById(R.id.faderWorkload); mSeekBar.setOnSeekBarChangeListener(mChangeListener); - mExponentialTaper = new ExponentialTaper(0.0, 100.0, 10.0); + setRange(0.0, 100.0); //mSeekBar.setProgress(0); } - private void setValueByPosition(int progress) { - double workload = mExponentialTaper.linearToExponential( - ((double)progress) / FADER_PROGRESS_MAX); - mAudioStreamTester.setWorkload(workload); - mTextView.setText("Workload = " + String.format("%6.2f", workload)); + void setRange(double dMin, double dMax) { + mExponentialTaper = new ExponentialTaper(dMin, dMax, 10.0); } - public void updateBufferSize() { - int progress = mSeekBar.getProgress(); - setValueByPosition(progress); + private void setValueByPosition(int progress) { + int workload = (int) mExponentialTaper.linearToExponential( + ((double)progress) / FADER_PROGRESS_MAX); + if (mWorkloadReceiver != null) { + mWorkloadReceiver.setWorkload(workload); + } + mTextView.setText(getLabel() + " = " + String.format(Locale.getDefault(), "%3d", workload)); } @Override @@ -111,4 +116,12 @@ public class WorkloadView extends LinearLayout { super.setEnabled(enabled); mSeekBar.setEnabled(enabled); } + + public String getLabel() { + return mLabel; + } + + public void setLabel(String label) { + this.mLabel = label; + } } diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/res/drawable/ic_launcher_background.xml b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 00000000..ca3826a4 --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/res/drawable/ic_launcher_foreground.xml b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 00000000..c6b72859 --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/res/drawable/ic_notification.xml b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/res/drawable/ic_notification.xml new file mode 100644 index 00000000..61f9d9c3 --- /dev/null +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/res/drawable/ic_notification.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/res/layout/activity_auto_glitches.xml b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/res/layout/activity_auto_glitches.xml index 3aea6733..69801638 100644 --- a/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/res/layout/activity_auto_glitches.xml +++ b/mobile/android/love/src/jni/oboe/apps/OboeTester/app/src/main/res/layout/activity_auto_glitches.xml @@ -1,14 +1,19 @@ - + + android:paddingTop="@dimen/activity_vertical_margin" > - - -