From 08acf6ff9bde398bf33c9bd7df12ad9466d963ab Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 2 Jul 2026 20:54:02 +0000 Subject: [PATCH 01/17] feat(usb): add :usbaudio native module scaffold with vendored libusb 1.0.30 New Gradle library module hosting the USB Audio Class driver for the upcoming USB exclusive (bit-perfect) output mode: - CMake/NDK build (r28 LTS, 16 KB-aligned, arm64-v8a + armeabi-v7a to match the app's ABI splits) - libusb 1.0.30 vendored as an unmodified pinned source subset (the same file list upstream's android/jni/libusb.mk compiles), built as a separate shared library for LGPL-2.1 compliance; see VENDORED.md - Thin JNI bridge exposing only nativeGetVersion() for now; the library is loaded lazily so the normal playback path never touches it Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FZMSPh3TgbRAMqgW4q1p6V --- app/build.gradle.kts | 1 + settings.gradle.kts | 1 + usbaudio/build.gradle.kts | 52 + usbaudio/src/main/cpp/CMakeLists.txt | 39 + usbaudio/src/main/cpp/external/libusb/COPYING | 504 +++ .../src/main/cpp/external/libusb/VENDORED.md | 19 + .../main/cpp/external/libusb/android/config.h | 55 + .../main/cpp/external/libusb/libusb/core.c | 3086 +++++++++++++++++ .../cpp/external/libusb/libusb/descriptor.c | 1707 +++++++++ .../main/cpp/external/libusb/libusb/hotplug.c | 489 +++ .../src/main/cpp/external/libusb/libusb/io.c | 2869 +++++++++++++++ .../main/cpp/external/libusb/libusb/libusb.h | 2471 +++++++++++++ .../main/cpp/external/libusb/libusb/libusbi.h | 1599 +++++++++ .../external/libusb/libusb/os/events_posix.c | 340 ++ .../external/libusb/libusb/os/events_posix.h | 62 + .../external/libusb/libusb/os/linux_netlink.c | 401 +++ .../external/libusb/libusb/os/linux_usbfs.c | 2877 +++++++++++++++ .../external/libusb/libusb/os/linux_usbfs.h | 221 ++ .../external/libusb/libusb/os/threads_posix.c | 126 + .../external/libusb/libusb/os/threads_posix.h | 104 + .../cpp/external/libusb/libusb/strerror.c | 223 ++ .../main/cpp/external/libusb/libusb/sync.c | 342 ++ .../main/cpp/external/libusb/libusb/version.h | 18 + .../cpp/external/libusb/libusb/version_nano.h | 1 + usbaudio/src/main/cpp/jni_bridge.cpp | 13 + .../pixelplay/usbaudio/UsbAudioNative.kt | 28 + 26 files changed, 17648 insertions(+) create mode 100644 usbaudio/build.gradle.kts create mode 100644 usbaudio/src/main/cpp/CMakeLists.txt create mode 100644 usbaudio/src/main/cpp/external/libusb/COPYING create mode 100644 usbaudio/src/main/cpp/external/libusb/VENDORED.md create mode 100644 usbaudio/src/main/cpp/external/libusb/android/config.h create mode 100644 usbaudio/src/main/cpp/external/libusb/libusb/core.c create mode 100644 usbaudio/src/main/cpp/external/libusb/libusb/descriptor.c create mode 100644 usbaudio/src/main/cpp/external/libusb/libusb/hotplug.c create mode 100644 usbaudio/src/main/cpp/external/libusb/libusb/io.c create mode 100644 usbaudio/src/main/cpp/external/libusb/libusb/libusb.h create mode 100644 usbaudio/src/main/cpp/external/libusb/libusb/libusbi.h create mode 100644 usbaudio/src/main/cpp/external/libusb/libusb/os/events_posix.c create mode 100644 usbaudio/src/main/cpp/external/libusb/libusb/os/events_posix.h create mode 100644 usbaudio/src/main/cpp/external/libusb/libusb/os/linux_netlink.c create mode 100644 usbaudio/src/main/cpp/external/libusb/libusb/os/linux_usbfs.c create mode 100644 usbaudio/src/main/cpp/external/libusb/libusb/os/linux_usbfs.h create mode 100644 usbaudio/src/main/cpp/external/libusb/libusb/os/threads_posix.c create mode 100644 usbaudio/src/main/cpp/external/libusb/libusb/os/threads_posix.h create mode 100644 usbaudio/src/main/cpp/external/libusb/libusb/strerror.c create mode 100644 usbaudio/src/main/cpp/external/libusb/libusb/sync.c create mode 100644 usbaudio/src/main/cpp/external/libusb/libusb/version.h create mode 100644 usbaudio/src/main/cpp/external/libusb/libusb/version_nano.h create mode 100644 usbaudio/src/main/cpp/jni_bridge.cpp create mode 100644 usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/UsbAudioNative.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 04d49b171..f476a2aa9 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -301,6 +301,7 @@ dependencies { // Projects implementation(project(":shared")) + implementation(project(":usbaudio")) // Testing (Unit) testImplementation(libs.junit.jupiter.api) diff --git a/settings.gradle.kts b/settings.gradle.kts index 15d093bfa..cb206124c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -33,5 +33,6 @@ dependencyResolutionManagement { rootProject.name = "PixelPlay" include(":app") include(":shared") +include(":usbaudio") include(":wear") include(":baselineprofile") diff --git a/usbaudio/build.gradle.kts b/usbaudio/build.gradle.kts new file mode 100644 index 000000000..365b083ca --- /dev/null +++ b/usbaudio/build.gradle.kts @@ -0,0 +1,52 @@ +plugins { + alias(libs.plugins.android.library) +} + +android { + namespace = "com.theveloper.pixelplay.usbaudio" + compileSdk = 37 + // r28 LTS: 16 KB page-size alignment is the default, required for targetSdk 37 devices. + ndkVersion = "28.2.13676358" + + defaultConfig { + minSdk = 30 // Must match app module's minSdk + + ndk { + // Keep in sync with the ABI splits declared in app/build.gradle.kts. + abiFilters += listOf("arm64-v8a", "armeabi-v7a") + } + } + + externalNativeBuild { + cmake { + path = file("src/main/cpp/CMakeLists.txt") + version = "3.22.1" + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 + } + + testOptions { + unitTests.all { it.useJUnitPlatform() } + } +} + +kotlin { + compilerOptions { + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21) + } +} + +dependencies { + implementation(libs.kotlinx.coroutines.core) + + testImplementation(libs.junit.jupiter.api) + testImplementation(libs.junit.jupiter.params) + testRuntimeOnly(libs.junit.jupiter.engine) + testRuntimeOnly(libs.junitplatformlauncher) + testImplementation(libs.kotlinx.coroutines.test) + testImplementation(libs.truth) +} diff --git a/usbaudio/src/main/cpp/CMakeLists.txt b/usbaudio/src/main/cpp/CMakeLists.txt new file mode 100644 index 000000000..8f71d4b5d --- /dev/null +++ b/usbaudio/src/main/cpp/CMakeLists.txt @@ -0,0 +1,39 @@ +cmake_minimum_required(VERSION 3.22.1) +project(usbaudio LANGUAGES C CXX) + +set(CMAKE_C_STANDARD 11) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# --- libusb (vendored, see external/libusb/VENDORED.md) ----------------------- +# Built as a SHARED library: libusb is LGPL-2.1, dynamic linking keeps the +# app's obligations to shipping the license text and this rebuildable copy. +set(LIBUSB_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/external/libusb) + +add_library(usb-1.0 SHARED + ${LIBUSB_ROOT}/libusb/core.c + ${LIBUSB_ROOT}/libusb/descriptor.c + ${LIBUSB_ROOT}/libusb/hotplug.c + ${LIBUSB_ROOT}/libusb/io.c + ${LIBUSB_ROOT}/libusb/sync.c + ${LIBUSB_ROOT}/libusb/strerror.c + ${LIBUSB_ROOT}/libusb/os/linux_usbfs.c + ${LIBUSB_ROOT}/libusb/os/events_posix.c + ${LIBUSB_ROOT}/libusb/os/threads_posix.c + ${LIBUSB_ROOT}/libusb/os/linux_netlink.c +) + +# android/config.h must come first so libusb's `#include ` finds it. +target_include_directories(usb-1.0 + PRIVATE ${LIBUSB_ROOT}/android ${LIBUSB_ROOT}/libusb/os + PUBLIC ${LIBUSB_ROOT}/libusb +) +target_compile_options(usb-1.0 PRIVATE -fvisibility=hidden -pthread) +target_link_libraries(usb-1.0 PRIVATE log) + +# --- JNI bridge ---------------------------------------------------------------- +add_library(usbaudio_jni SHARED + jni_bridge.cpp +) + +target_link_libraries(usbaudio_jni PRIVATE usb-1.0 log) diff --git a/usbaudio/src/main/cpp/external/libusb/COPYING b/usbaudio/src/main/cpp/external/libusb/COPYING new file mode 100644 index 000000000..5ab7695ab --- /dev/null +++ b/usbaudio/src/main/cpp/external/libusb/COPYING @@ -0,0 +1,504 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! + + diff --git a/usbaudio/src/main/cpp/external/libusb/VENDORED.md b/usbaudio/src/main/cpp/external/libusb/VENDORED.md new file mode 100644 index 000000000..0a5fd2be4 --- /dev/null +++ b/usbaudio/src/main/cpp/external/libusb/VENDORED.md @@ -0,0 +1,19 @@ +# Vendored libusb + +Upstream: https://github.com/libusb/libusb +Version: 1.0.30 (unmodified sources) +Source tarball: `libusb-1.0_1.0.30.orig.tar.bz2` (upstream release tarball as +mirrored by Debian: https://deb.debian.org/debian/pool/main/libu/libusb-1.0/) + +Only the subset needed for the Android/Linux backend is vendored — the same +file list libusb's own `android/jni/libusb.mk` compiles: + +- `libusb/{core,descriptor,hotplug,io,sync,strerror}.c` and public/private headers +- `libusb/os/{linux_usbfs,events_posix,threads_posix,linux_netlink}.c` and headers +- `android/config.h` (libusb's own Android build configuration) +- `COPYING` (LGPL-2.1 license text — libusb is built and shipped as a separate + shared library, `libusb-1.0.so`) + +To update: download the new upstream release tarball, replace the files above +1:1, and update the version in this file. Do not patch the sources; anything +Android-specific belongs in our CMakeLists.txt or our own code. diff --git a/usbaudio/src/main/cpp/external/libusb/android/config.h b/usbaudio/src/main/cpp/external/libusb/android/config.h new file mode 100644 index 000000000..1092f65e5 --- /dev/null +++ b/usbaudio/src/main/cpp/external/libusb/android/config.h @@ -0,0 +1,55 @@ +/* + * Android build config for libusb + * Copyright © 2012-2013 RealVNC Ltd. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +/* Define to the attribute for default visibility. */ +#define DEFAULT_VISIBILITY __attribute__ ((visibility ("default"))) + +/* Define to 1 to start with debug message logging enabled. */ +/* #undef ENABLE_DEBUG_LOGGING */ + +/* Define to 1 to enable message logging. */ +#define ENABLE_LOGGING 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_ASM_TYPES_H 1 + +/* Define to 1 if you have the `clock_gettime' function. */ +#define HAVE_CLOCK_GETTIME 1 + +/* Define to 1 if the system has the type `nfds_t'. */ +#define HAVE_NFDS_T 1 + +/* Define to 1 if you have the `pipe2' function. */ +#define HAVE_PIPE2 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_SYS_TIME_H 1 + +/* Define to 1 if compiling for a POSIX platform. */ +#define PLATFORM_POSIX 1 + +/* Define to the attribute for enabling parameter checks on printf-like + functions. */ +#define PRINTF_FORMAT(a, b) __attribute__ ((__format__ (__printf__, a, b))) + +/* Define to 1 to output logging messages to the systemwide log. */ +#define USE_SYSTEM_LOGGING_FACILITY 1 + +/* Enable GNU extensions. */ +#define _GNU_SOURCE 1 diff --git a/usbaudio/src/main/cpp/external/libusb/libusb/core.c b/usbaudio/src/main/cpp/external/libusb/libusb/core.c new file mode 100644 index 000000000..507066af1 --- /dev/null +++ b/usbaudio/src/main/cpp/external/libusb/libusb/core.c @@ -0,0 +1,3086 @@ +/* -*- Mode: C; indent-tabs-mode:t ; c-basic-offset:8 -*- */ +/* + * Core functions for libusb + * Copyright © 2012-2023 Nathan Hjelm + * Copyright © 2007-2008 Daniel Drake + * Copyright © 2001 Johannes Erdfelt + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "libusbi.h" +#include "version.h" + +#ifdef __ANDROID__ +#include +#endif +#include +#include +#ifdef HAVE_SYSLOG +#include +#endif + +static const struct libusb_version libusb_version_internal = + { LIBUSB_MAJOR, LIBUSB_MINOR, LIBUSB_MICRO, LIBUSB_NANO, + LIBUSB_RC, "https://libusb.info" }; +static struct timespec timestamp_origin; +#if defined(ENABLE_LOGGING) && !defined(USE_SYSTEM_LOGGING_FACILITY) +static libusb_log_cb log_handler; +#endif + +struct libusb_context *usbi_default_context; +struct libusb_context *usbi_fallback_context; +static int default_context_refcnt; +#if defined(ENABLE_LOGGING) && !defined(ENABLE_DEBUG_LOGGING) +static usbi_atomic_t default_debug_level = -1; +#endif +static usbi_mutex_static_t default_context_lock = USBI_MUTEX_INITIALIZER; +static struct usbi_option default_context_options[LIBUSB_OPTION_MAX]; + + +usbi_mutex_static_t active_contexts_lock = USBI_MUTEX_INITIALIZER; +struct list_head active_contexts_list; + +/** + * \mainpage libusb-1.0 API Reference + * + * \section intro Introduction + * + * libusb is an open source library that allows you to communicate with USB + * devices from user space. For more info, see the + * libusb homepage. + * + * This documentation is aimed at application developers wishing to + * communicate with USB peripherals from their own software. After reviewing + * this documentation, feedback and questions can be sent to the + * libusb-devel mailing list. + * + * This documentation assumes knowledge of how to operate USB devices from + * a software standpoint (descriptors, configurations, interfaces, endpoints, + * control/bulk/interrupt/isochronous transfers, etc). Full information + * can be found in the USB 3.0 + * Specification which is available for free download. You can probably + * find less verbose introductions by searching the web. + * + * \section API Application Programming Interface (API) + * + * See the \ref libusb_api page for a complete list of the libusb functions. + * + * \section features Library features + * + * - All transfer types supported (control/bulk/interrupt/isochronous) + * - 2 transfer interfaces: + * -# Synchronous (simple) + * -# Asynchronous (more complicated, but more powerful) + * - Thread safe (although the asynchronous interface means that you + * usually won't need to thread) + * - Lightweight with lean API + * - Compatible with libusb-0.1 through the libusb-compat-0.1 translation layer + * - Hotplug support (on some platforms). See \ref libusb_hotplug. + * + * \section gettingstarted Getting Started + * + * To begin reading the API documentation, start with the Topics page which + * links to the different categories of libusb's functionality. + * + * One decision you will have to make is whether to use the synchronous + * or the asynchronous data transfer interface. The \ref libusb_io documentation + * provides some insight into this topic. + * + * Some example programs can be found in the libusb source distribution under + * the "examples" subdirectory. The libusb homepage includes a list of + * real-life project examples which use libusb. + * + * \section errorhandling Error handling + * + * libusb functions typically return 0 on success or a negative error code + * on failure. These negative error codes relate to LIBUSB_ERROR constants + * which are listed on the \ref libusb_misc "miscellaneous" documentation page. + * + * \section msglog Debug message logging + * + * libusb uses stderr for all logging. By default, logging is set to NONE, + * which means that no output will be produced. However, unless the library + * has been compiled with logging disabled, then any application calls to + * libusb_set_option(ctx, LIBUSB_OPTION_LOG_LEVEL, level), + * libusb_init_context, or the setting of the environmental variable + * LIBUSB_DEBUG outside of the application, can result in logging being + * produced. Your application should therefore not close stderr, but instead + * direct it to the null device if its output is undesirable. + * + * The libusb_set_option(ctx, LIBUSB_OPTION_LOG_LEVEL, level) or + * libusb_init_context functions can be used to enable logging of certain + * messages. With the default configuration, libusb will not log much so if + * you are advised to use one of these functions to enable all + * error/warning/informational messages. It will help debug problems with your + * software. + * + * The logged messages are unstructured. There is no one-to-one correspondence + * between messages being logged and success or failure return codes from + * libusb functions. There is no format to the messages, so you should not + * try to capture or parse them. They are not and will not be localized. + * These messages are not intended to being passed to your application user; + * instead, you should interpret the error codes returned from libusb functions + * and provide appropriate notification to the user. The messages are simply + * there to aid you as a programmer, and if you're confused because you're + * getting a strange error code from a libusb function, enabling message + * logging may give you a suitable explanation. + * + * The LIBUSB_DEBUG environment variable can be used to enable message logging + * at run-time. This environment variable should be set to a log level number, + * which is interpreted the same as the + * libusb_set_option(ctx, LIBUSB_OPTION_LOG_LEVEL, level), or + * libusb_init_context(&ctx, &(struct libusb_init_option){.option = LIBUSB_OPTION_LOG_LEVEL, .value = {.ival = level}}, 0). + * When the environment variable is set, the message logging verbosity level is + * fixed and setting the LIBUSB_OPTION_LOG_LEVEL option has no effect. + * + * libusb can be compiled without any logging functions, useful for embedded + * systems. In this case, neither the LIBUSB_OPTION_LOG_LEVEL option, nor the + * LIBUSB_DEBUG environment variable will have any effect. + * + * libusb can also be compiled with verbose debugging messages always. When + * the library is compiled in this way, all messages of all verbosities are + * always logged. Again, in this case, neither the LIBUSB_OPTION_LOG_LEVEL + * option, nor the LIBUSB_DEBUG environment variable will have any effect. + * + * \section remarks Other remarks + * + * libusb does have imperfections. The \ref libusb_caveats "caveats" page attempts + * to document these. + */ + +/** + * \page libusb_caveats Caveats + * + * \section threadsafety Thread safety + * + * libusb is designed to be completely thread-safe, but as with any API it + * cannot prevent a user from sabotaging themselves, either intentionally or + * otherwise. + * + * Observe the following general guidelines: + * + * - Calls to functions that release a resource (e.g. libusb_close(), + * libusb_free_config_descriptor()) should not be called concurrently on + * the same resource. This is no different than concurrently calling free() + * on the same allocated pointer. + * - Each individual \ref libusb_transfer should be prepared by a single + * thread. In other words, no two threads should ever be concurrently + * filling out the fields of a \ref libusb_transfer. You can liken this to + * calling sprintf() with the same destination buffer from multiple threads. + * The results will likely not be what you want unless the input parameters + * are all the same, but its best to avoid this situation entirely. + * - Both the \ref libusb_transfer structure and its associated data buffer + * should not be accessed between the time the transfer is submitted and the + * time the completion callback is invoked. You can think of "ownership" of + * these things as being transferred to libusb while the transfer is active. + * - The various "setter" functions (e.g. libusb_set_log_cb(), + * libusb_set_pollfd_notifiers()) should not be called concurrently on the + * resource. Though doing so will not lead to any undefined behavior, it + * will likely produce results that the application does not expect. + * + * Rules for multiple threads and asynchronous I/O are detailed + * \ref libusb_mtasync "here". + * + * \section fork Fork considerations + * + * libusb is not designed to work across fork() calls. Depending on + * the platform, there may be resources in the parent process that are not + * available to the child (e.g. the hotplug monitor thread on Linux). In + * addition, since the parent and child will share libusb's internal file + * descriptors, using libusb in any way from the child could cause the parent + * process's \ref libusb_context to get into an inconsistent state. + * + * On Linux, libusb's file descriptors will be marked as CLOEXEC, which means + * that it is safe to fork() and exec() without worrying about the child + * process needing to clean up state or having access to these file descriptors. + * Other platforms may not be so forgiving, so consider yourself warned! + * + * \section devresets Device resets + * + * The libusb_reset_device() function allows you to reset a device. If your + * program has to call such a function, it should obviously be aware that + * the reset will cause device state to change (e.g. register values may be + * reset). + * + * The problem is that any other program could reset the device your program + * is working with, at any time. libusb does not offer a mechanism to inform + * you when this has happened, so if someone else resets your device it will + * not be clear to your own program why the device state has changed. + * + * Ultimately, this is a limitation of writing drivers in user space. + * Separation from the USB stack in the underlying kernel makes it difficult + * for the operating system to deliver such notifications to your program. + * The Linux kernel USB stack allows such reset notifications to be delivered + * to in-kernel USB drivers, but it is not clear how such notifications could + * be delivered to second-class drivers that live in user space. + * + * \section blockonly Blocking-only functionality + * + * The functionality listed below is only available through synchronous, + * blocking functions. There are no asynchronous/non-blocking alternatives, + * and no clear ways of implementing these. + * + * - Configuration activation (libusb_set_configuration()) + * - Interface/alternate setting activation (libusb_set_interface_alt_setting()) + * - Releasing of interfaces (libusb_release_interface()) + * - Clearing of halt/stall condition (libusb_clear_halt()) + * - Device resets (libusb_reset_device()) + * + * \section configsel Configuration selection and handling + * + * When libusb presents a device handle to an application, there is a chance + * that the corresponding device may be in unconfigured state. For devices + * with multiple configurations, there is also a chance that the configuration + * currently selected is not the one that the application wants to use. + * + * The obvious solution is to add a call to libusb_set_configuration() early + * on during your device initialization routines, but there are caveats to + * be aware of: + * -# If the device is already in the desired configuration, calling + * libusb_set_configuration() using the same configuration value will cause + * a lightweight device reset. This may not be desirable behaviour. + * -# In the case where the desired configuration is already active, libusb + * may not even be able to perform a lightweight device reset. For example, + * take my USB keyboard with fingerprint reader: I'm interested in driving + * the fingerprint reader interface through libusb, but the kernel's + * USB-HID driver will almost always have claimed the keyboard interface. + * Because the kernel has claimed an interface, it is not even possible to + * perform the lightweight device reset, so libusb_set_configuration() will + * fail. (Luckily the device in question only has a single configuration.) + * -# libusb will be unable to set a configuration if other programs or + * drivers have claimed interfaces. In particular, this means that kernel + * drivers must be detached from all the interfaces before + * libusb_set_configuration() may succeed. + * + * One solution to some of the above problems is to consider the currently + * active configuration. If the configuration we want is already active, then + * we don't have to select any configuration: +\code +cfg = -1; +libusb_get_configuration(dev, &cfg); +if (cfg != desired) + libusb_set_configuration(dev, desired); +\endcode + * + * This is probably suitable for most scenarios, but is inherently racy: + * another application or driver may change the selected configuration + * after the libusb_get_configuration() call. + * + * Even in cases where libusb_set_configuration() succeeds, consider that other + * applications or drivers may change configuration after your application + * calls libusb_set_configuration(). + * + * One possible way to lock your device into a specific configuration is as + * follows: + * -# Set the desired configuration (or use the logic above to realise that + * it is already in the desired configuration) + * -# Claim the interface that you wish to use + * -# Check that the currently active configuration is the one that you want + * to use. + * + * The above method works because once an interface is claimed, no application + * or driver is able to select another configuration. + * + * \section earlycomp Early transfer completion + * + * NOTE: This section is currently Linux-centric. I am not sure if any of these + * considerations apply to Darwin or other platforms. + * + * When a transfer completes early (i.e. when less data is received/sent in + * any one packet than the transfer buffer allows for) then libusb is designed + * to terminate the transfer immediately, not transferring or receiving any + * more data unless other transfers have been queued by the user. + * + * On legacy platforms, libusb is unable to do this in all situations. After + * the incomplete packet occurs, "surplus" data may be transferred. For recent + * versions of libusb, this information is kept (the data length of the + * transfer is updated) and, for device-to-host transfers, any surplus data was + * added to the buffer. Still, this is not a nice solution because it loses the + * information about the end of the short packet, and the user probably wanted + * that surplus data to arrive in the next logical transfer. + * + * \section zlp Zero length packets + * + * - libusb is able to send a packet of zero length to an endpoint simply by + * submitting a transfer of zero length. + * - The \ref libusb_transfer_flags::LIBUSB_TRANSFER_ADD_ZERO_PACKET + * "LIBUSB_TRANSFER_ADD_ZERO_PACKET" flag is currently supported on Linux, + * Darwin and Windows (WinUSB). + */ + +/** + * \page libusb_contexts Contexts + * + * It is possible that libusb may be used simultaneously from two independent + * libraries linked into the same executable. For example, if your application + * has a plugin-like system which allows the user to dynamically load a range + * of modules into your program, it is feasible that two independently + * developed modules may both use libusb. + * + * libusb is written to allow for these multiple user scenarios. The two + * "instances" of libusb will not interfere: an option set by one user will have + * no effect the same option for other users, other users can continue using + * libusb after one of them calls libusb_exit(), etc. + * + * This is made possible through libusb's context concept. When you + * call libusb_init_context(), you are (optionally) given a context. You can then pass + * this context pointer back into future libusb functions. + * + * In order to keep things simple for more simplistic applications, it is + * legal to pass NULL to all functions requiring a context pointer (as long as + * you're sure no other code will attempt to use libusb from the same process). + * When you pass NULL, the default context will be used. The default context + * is created the first time a process calls libusb_init_context() when no other + * context is alive. Contexts are destroyed during libusb_exit(). + * + * The default context is reference-counted and can be shared. That means that + * if libusb_init_context(NULL, x, y) is called twice within the same process, the two + * users end up sharing the same context. The deinitialization and freeing of + * the default context will only happen when the last user calls libusb_exit(). + * In other words, the default context is created and initialized when its + * reference count goes from 0 to 1, and is deinitialized and destroyed when + * its reference count goes from 1 to 0. + * + * You may be wondering why only a subset of libusb functions require a + * context pointer in their function definition. Internally, libusb stores + * context pointers in other objects (e.g. libusb_device instances) and hence + * can infer the context from those objects. + */ + + /** + * \page libusb_api Application Programming Interface + * + * This is the complete list of libusb functions, structures and + * enumerations in alphabetical order. + * + * \section Functions + * - libusb_alloc_streams() + * - libusb_alloc_transfer() + * - libusb_attach_kernel_driver() + * - libusb_bulk_transfer() + * - libusb_cancel_transfer() + * - libusb_claim_interface() + * - libusb_clear_halt() + * - libusb_close() + * - libusb_control_transfer() + * - libusb_control_transfer_get_data() + * - libusb_control_transfer_get_setup() + * - libusb_cpu_to_le16() + * - libusb_detach_kernel_driver() + * - libusb_dev_mem_alloc() + * - libusb_dev_mem_free() + * - libusb_endpoint_set_raw_io() + * - libusb_endpoint_supports_raw_io() + * - libusb_error_name() + * - libusb_event_handler_active() + * - libusb_event_handling_ok() + * - libusb_exit() + * - libusb_fill_bulk_stream_transfer() + * - libusb_fill_bulk_transfer() + * - libusb_fill_control_setup() + * - libusb_fill_control_transfer() + * - libusb_fill_interrupt_transfer() + * - libusb_fill_iso_transfer() + * - libusb_free_bos_descriptor() + * - libusb_free_config_descriptor() + * - libusb_free_container_id_descriptor() + * - libusb_free_device_list() + * - libusb_free_pollfds() + * - libusb_free_ss_endpoint_companion_descriptor() + * - libusb_free_ss_usb_device_capability_descriptor() + * - libusb_free_streams() + * - libusb_free_transfer() + * - libusb_free_usb_2_0_extension_descriptor() + * - libusb_get_active_config_descriptor() + * - libusb_get_bos_descriptor() + * - libusb_get_session_data() + * - libusb_get_bus_number() + * - libusb_get_config_descriptor() + * - libusb_get_config_descriptor_by_value() + * - libusb_get_configuration() + * - libusb_get_container_id_descriptor() + * - libusb_get_descriptor() + * - libusb_get_device() + * - libusb_get_device_address() + * - libusb_get_device_descriptor() + * - libusb_get_device_list() + * - libusb_get_device_speed() + * - libusb_get_device_string() + * - libusb_get_iso_packet_buffer() + * - libusb_get_iso_packet_buffer_simple() + * - libusb_get_max_alt_packet_size() + * - libusb_get_max_iso_packet_size() + * - libusb_get_max_raw_io_transfer_size() + * - libusb_get_max_packet_size() + * - libusb_get_next_timeout() + * - libusb_get_parent() + * - libusb_get_pollfds() + * - libusb_get_port_number() + * - libusb_get_port_numbers() + * - libusb_get_port_path() + * - libusb_get_ss_endpoint_companion_descriptor() + * - libusb_get_ss_usb_device_capability_descriptor() + * - libusb_get_string_descriptor() + * - libusb_get_string_descriptor_ascii() + * - libusb_get_usb_2_0_extension_descriptor() + * - libusb_get_version() + * - libusb_handle_events() + * - libusb_handle_events_completed() + * - libusb_handle_events_locked() + * - libusb_handle_events_timeout() + * - libusb_handle_events_timeout_completed() + * - libusb_has_capability() + * - libusb_hotplug_deregister_callback() + * - libusb_hotplug_register_callback() + * - libusb_init() + * - libusb_init_context() + * - libusb_interrupt_event_handler() + * - libusb_interrupt_transfer() + * - libusb_kernel_driver_active() + * - libusb_lock_events() + * - libusb_lock_event_waiters() + * - libusb_open() + * - libusb_open_device_with_vid_pid() + * - libusb_pollfds_handle_timeouts() + * - libusb_ref_device() + * - libusb_release_interface() + * - libusb_reset_device() + * - libusb_set_auto_detach_kernel_driver() + * - libusb_set_configuration() + * - libusb_set_debug() + * - libusb_set_log_cb() + * - libusb_set_interface_alt_setting() + * - libusb_set_iso_packet_lengths() + * - libusb_set_option() + * - libusb_setlocale() + * - libusb_set_pollfd_notifiers() + * - libusb_strerror() + * - libusb_submit_transfer() + * - libusb_transfer_get_stream_id() + * - libusb_transfer_set_stream_id() + * - libusb_try_lock_events() + * - libusb_unlock_events() + * - libusb_unlock_event_waiters() + * - libusb_unref_device() + * - libusb_wait_for_event() + * - libusb_wrap_sys_device() + * + * \section Structures + * - libusb_bos_descriptor + * - libusb_bos_dev_capability_descriptor + * - libusb_config_descriptor + * - libusb_container_id_descriptor + * - \ref libusb_context + * - libusb_control_setup + * - \ref libusb_device + * - libusb_device_descriptor + * - \ref libusb_device_handle + * - libusb_endpoint_descriptor + * - libusb_interface + * - libusb_interface_descriptor + * - libusb_iso_packet_descriptor + * - libusb_pollfd + * - libusb_ss_endpoint_companion_descriptor + * - libusb_ss_usb_device_capability_descriptor + * - libusb_transfer + * - libusb_usb_2_0_extension_descriptor + * - libusb_version + * + * \section Enums + * - \ref libusb_bos_type + * - \ref libusb_capability + * - \ref libusb_class_code + * - \ref libusb_descriptor_type + * - \ref libusb_device_string_type + * - \ref libusb_endpoint_direction + * - \ref libusb_endpoint_transfer_type + * - \ref libusb_error + * - \ref libusb_iso_sync_type + * - \ref libusb_iso_usage_type + * - \ref libusb_log_level + * - \ref libusb_option + * - \ref libusb_request_recipient + * - \ref libusb_request_type + * - \ref libusb_speed + * - \ref libusb_ss_usb_device_capability_attributes + * - \ref libusb_standard_request + * - \ref libusb_supported_speed + * - \ref libusb_transfer_flags + * - \ref libusb_transfer_status + * - \ref libusb_transfer_type + * - \ref libusb_usb_2_0_extension_attributes + */ + +/** + * @defgroup libusb_lib Library initialization/deinitialization + * This page details how to initialize and deinitialize libusb. Initialization + * must be performed before using any libusb functionality, and similarly you + * must not call any libusb functions after deinitialization. + */ + +/** + * @defgroup libusb_dev Device handling and enumeration + * The functionality documented below is designed to help with the following + * operations: + * - Enumerating the USB devices currently attached to the system + * - Choosing a device to operate from your software + * - Opening and closing the chosen device + * + * \section nutshell In a nutshell... + * + * The description below really makes things sound more complicated than they + * actually are. The following sequence of function calls will be suitable + * for almost all scenarios and does not require you to have such a deep + * understanding of the resource management issues: + * \code +// discover devices +libusb_device **list; +libusb_device *found = NULL; +ssize_t cnt = libusb_get_device_list(NULL, &list); +ssize_t i = 0; +int err = 0; +if (cnt < 0) + error(); + +for (i = 0; i < cnt; i++) { + libusb_device *device = list[i]; + if (is_interesting(device)) { + found = device; + break; + } +} + +if (found) { + libusb_device_handle *handle; + + err = libusb_open(found, &handle); + if (err) + error(); + // etc +} + +libusb_free_device_list(list, 1); +\endcode + * + * The two important points: + * - You asked libusb_free_device_list() to unreference the devices (2nd + * parameter) + * - You opened the device before freeing the list and unreferencing the + * devices + * + * If you ended up with a handle, you can now proceed to perform I/O on the + * device. + * + * \section devshandles Devices and device handles + * libusb has a concept of a USB device, represented by the + * \ref libusb_device opaque type. A device represents a USB device that + * is currently or was previously connected to the system. Using a reference + * to a device, you can determine certain information about the device (e.g. + * you can read the descriptor data). + * + * The libusb_get_device_list() function can be used to obtain a list of + * devices currently connected to the system. This is known as device + * discovery. Devices can also be discovered with the hotplug mechanism, + * whereby a callback function registered with libusb_hotplug_register_callback() + * will be called when a device of interest is connected or disconnected. + * + * Just because you have a reference to a device does not mean it is + * necessarily usable. The device may have been unplugged, you may not have + * permission to operate such device, or another program or driver may be + * using the device. + * + * When you've found a device that you'd like to operate, you must ask + * libusb to open the device using the libusb_open() function. Assuming + * success, libusb then returns you a device handle + * (a \ref libusb_device_handle pointer). All "real" I/O operations then + * operate on the handle rather than the original device pointer. + * + * \section devref Device discovery and reference counting + * + * Device discovery (i.e. calling libusb_get_device_list()) returns a + * freshly-allocated list of devices. The list itself must be freed when + * you are done with it. libusb also needs to know when it is OK to free + * the contents of the list - the devices themselves. + * + * To handle these issues, libusb provides you with two separate items: + * - A function to free the list itself + * - A reference counting system for the devices inside + * + * New devices presented by the libusb_get_device_list() function all have a + * reference count of 1. You can increase and decrease reference count using + * libusb_ref_device() and libusb_unref_device(). A device is destroyed when + * its reference count reaches 0. + * + * With the above information in mind, the process of opening a device can + * be viewed as follows: + * -# Discover devices using libusb_get_device_list() or libusb_hotplug_register_callback(). + * -# Choose the device that you want to operate, and call libusb_open(). + * -# Unref all devices in the discovered device list. + * -# Free the discovered device list. + * + * The order is important - you must not unreference the device before + * attempting to open it, because unreferencing it may destroy the device. + * + * For convenience, the libusb_free_device_list() function includes a + * parameter to optionally unreference all the devices in the list before + * freeing the list itself. This combines steps 3 and 4 above. + * + * As an implementation detail, libusb_open() actually adds a reference to + * the device in question. This is because the device remains available + * through the handle via libusb_get_device(). The reference is deleted during + * libusb_close(). + */ + +/** @defgroup libusb_misc Miscellaneous */ + +/* we traverse usbfs without knowing how many devices we are going to find. + * so we create this discovered_devs model which is similar to a linked-list + * which grows when required. it can be freed once discovery has completed, + * eliminating the need for a list node in the libusb_device structure + * itself. */ +#define DISCOVERED_DEVICES_SIZE_STEP 16 + +static struct discovered_devs *discovered_devs_alloc(void) +{ + struct discovered_devs *ret = + malloc(sizeof(*ret) + (sizeof(void *) * DISCOVERED_DEVICES_SIZE_STEP)); + + if (ret) { + ret->len = 0; + ret->capacity = DISCOVERED_DEVICES_SIZE_STEP; + } + return ret; +} + +static void discovered_devs_free(struct discovered_devs *discdevs) +{ + size_t i; + + for (i = 0; i < discdevs->len; i++) + libusb_unref_device(discdevs->devices[i]); + + free(discdevs); +} + +/* append a device to the discovered devices collection. may realloc itself, + * returning new discdevs. returns NULL on realloc failure. */ +struct discovered_devs *discovered_devs_append( + struct discovered_devs *discdevs, struct libusb_device *dev) +{ + size_t len = discdevs->len; + size_t capacity; + struct discovered_devs *new_discdevs; + + /* if there is space, just append the device */ + if (len < discdevs->capacity) { + discdevs->devices[len] = libusb_ref_device(dev); + discdevs->len++; + return discdevs; + } + + /* exceeded capacity, need to grow */ + usbi_dbg(DEVICE_CTX(dev), "need to increase capacity"); + capacity = discdevs->capacity + DISCOVERED_DEVICES_SIZE_STEP; + /* can't use usbi_reallocf here because in failure cases it would + * free the existing discdevs without unreferencing its devices. */ + new_discdevs = realloc(discdevs, + sizeof(*discdevs) + (sizeof(void *) * capacity)); + if (!new_discdevs) { + discovered_devs_free(discdevs); + return NULL; + } + + discdevs = new_discdevs; + discdevs->capacity = capacity; + discdevs->devices[len] = libusb_ref_device(dev); + discdevs->len++; + + return discdevs; +} + +/* Allocate a new device with a specific session ID. The returned device has + * a reference count of 1. */ +struct libusb_device *usbi_alloc_device(struct libusb_context *ctx, + unsigned long session_id) +{ + size_t priv_size = usbi_backend.device_priv_size; + struct libusb_device *dev = calloc(1, PTR_ALIGN(sizeof(*dev)) + priv_size); + + if (!dev) + return NULL; + + usbi_atomic_store(&dev->refcnt, 1); + + dev->ctx = ctx; + dev->session_data = session_id; + dev->speed = LIBUSB_SPEED_UNKNOWN; + + /* Note: the device is NOT added to ctx->usb_devs here. The caller + * (backend) must call usbi_connect_device() after fully initializing + * the device's private data. This prevents a concurrent + * usbi_get_device_by_session_id() from finding a half-initialized + * device. */ + + return dev; +} + +void usbi_attach_device(struct libusb_device *dev) +{ + struct libusb_context *ctx = DEVICE_CTX(dev); + + usbi_atomic_store(&dev->attached, 1); + + usbi_mutex_lock(&ctx->usb_devs_lock); + list_add(&dev->list, &ctx->usb_devs); + usbi_mutex_unlock(&ctx->usb_devs_lock); +} + +void usbi_connect_device(struct libusb_device *dev) +{ + usbi_attach_device(dev); + usbi_hotplug_notification(DEVICE_CTX(dev), dev, LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED); +} + +void usbi_detach_device(struct libusb_device *dev) +{ + struct libusb_context *ctx = DEVICE_CTX(dev); + + usbi_atomic_store(&dev->attached, 0); + + usbi_mutex_lock(&ctx->usb_devs_lock); + list_del(&dev->list); + usbi_mutex_unlock(&ctx->usb_devs_lock); +} + +void usbi_disconnect_device(struct libusb_device *dev) +{ + usbi_detach_device(dev); + usbi_hotplug_notification(DEVICE_CTX(dev), dev, LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT); +} + +/* Perform some final sanity checks on a newly discovered device. If this + * function fails (negative return code), the device should not be added + * to the discovered device list. */ +int usbi_sanitize_device(struct libusb_device *dev) +{ + uint8_t num_configurations; + + if (dev->device_descriptor.bLength != LIBUSB_DT_DEVICE_SIZE || + dev->device_descriptor.bDescriptorType != LIBUSB_DT_DEVICE) { + usbi_err(DEVICE_CTX(dev), "invalid device descriptor"); + return LIBUSB_ERROR_IO; + } + + num_configurations = dev->device_descriptor.bNumConfigurations; + if (num_configurations > USB_MAXCONFIG) { + usbi_err(DEVICE_CTX(dev), "too many configurations"); + return LIBUSB_ERROR_IO; + } else if (0 == num_configurations) { + usbi_dbg(DEVICE_CTX(dev), "zero configurations, maybe an unauthorized device"); + } + + return 0; +} + +/* Examine libusb's internal list of known devices, looking for one with + * a specific session ID. Returns the matching device if it was found, and + * NULL otherwise. */ +struct libusb_device *usbi_get_device_by_session_id(struct libusb_context *ctx, + unsigned long session_id) +{ + struct libusb_device *dev; + struct libusb_device *ret = NULL; + + usbi_mutex_lock(&ctx->usb_devs_lock); + for_each_device(ctx, dev) { + if (dev->session_data == session_id) { + ret = libusb_ref_device(dev); + break; + } + } + usbi_mutex_unlock(&ctx->usb_devs_lock); + + return ret; +} + +/** @ingroup libusb_dev + * Returns a list of USB devices currently attached to the system. This is + * your entry point into finding a USB device to operate. + * + * You are expected to unreference all the devices when you are done with + * them, and then free the list with libusb_free_device_list(). Note that + * libusb_free_device_list() can unref all the devices for you. Be careful + * not to unreference a device you are about to open until after you have + * opened it. + * + * This return value of this function indicates the number of devices in + * the resultant list. The list is actually one element larger, as it is + * NULL-terminated. + * + * \param ctx the context to operate on, or NULL for the default context + * \param list output location for a list of devices. Must be later freed with + * libusb_free_device_list(). + * \returns the number of devices in the outputted list, or any + * \ref libusb_error according to errors encountered by the backend. + */ +ssize_t API_EXPORTED libusb_get_device_list(libusb_context *ctx, + libusb_device ***list) +{ + struct discovered_devs *discdevs = discovered_devs_alloc(); + struct libusb_device **ret; + int r = 0; + ssize_t i, len; + + usbi_dbg(ctx, " "); + + if (!discdevs) + return LIBUSB_ERROR_NO_MEM; + + ctx = usbi_get_context(ctx); + + if (libusb_has_capability(LIBUSB_CAP_HAS_HOTPLUG)) { + /* backend provides hotplug support */ + struct libusb_device *dev; + + if (usbi_backend.hotplug_poll) + usbi_backend.hotplug_poll(); + + usbi_mutex_lock(&ctx->usb_devs_lock); + for_each_device(ctx, dev) { + discdevs = discovered_devs_append(discdevs, dev); + + if (!discdevs) { + r = LIBUSB_ERROR_NO_MEM; + break; + } + } + usbi_mutex_unlock(&ctx->usb_devs_lock); + } else { + /* backend does not provide hotplug support */ + r = usbi_backend.get_device_list(ctx, &discdevs); + } + + if (r < 0) { + len = r; + goto out; + } + + /* convert discovered_devs into a list */ + len = (ssize_t)discdevs->len; + ret = calloc((size_t)len + 1, sizeof(struct libusb_device *)); + if (!ret) { + len = LIBUSB_ERROR_NO_MEM; + goto out; + } + + ret[len] = NULL; + for (i = 0; i < len; i++) { + struct libusb_device *dev = discdevs->devices[i]; + ret[i] = libusb_ref_device(dev); + } + *list = ret; + +out: + if (discdevs) + discovered_devs_free(discdevs); + return len; +} + +/** \ingroup libusb_dev + * Frees a list of devices previously discovered using + * libusb_get_device_list(). If the unref_devices parameter is set, the + * reference count of each device in the list is decremented by 1. + * \param list the list to free + * \param unref_devices whether to unref the devices in the list + */ +void API_EXPORTED libusb_free_device_list(libusb_device **list, + int unref_devices) +{ + if (!list) + return; + + if (unref_devices) { + int i = 0; + struct libusb_device *dev; + + while ((dev = list[i++]) != NULL) + libusb_unref_device(dev); + } + free(list); +} + +/** \ingroup libusb_dev + * Returns the backend-specific identifier of the underlying system device tree + * node. Can be used to find the corresponding system device and directly query + * it (or access it otherwise) when and if necessary. + * + * Relevant backends: + * - Darwin: IOKit `sessionID` + * - Windows WinUSB: `DEVINST` + * - Linux, BSD: `busnum << 8 | devnum` + * + * Since version 1.0.30, \ref LIBUSB_API_VERSION >= 0x0100010C + * + * \param dev a device (must not be null) + * \returns the backend-specific device identifier + */ +unsigned long API_EXPORTED libusb_get_session_data(libusb_device *dev) +{ + return dev->session_data; +} + +/** \ingroup libusb_dev + * Get the number of the bus that a device is connected to. + * \param dev a device + * \returns the bus number + */ +uint8_t API_EXPORTED libusb_get_bus_number(libusb_device *dev) +{ + return dev->bus_number; +} + +/** \ingroup libusb_dev + * Get the number of the port that a device is connected to. + * Unless the OS does something funky, or you are hot-plugging USB extension cards, + * the port number returned by this call is usually guaranteed to be uniquely tied + * to a physical port, meaning that different devices plugged on the same physical + * port should return the same port number. + * + * But outside of this, there is no guarantee that the port number returned by this + * call will remain the same, or even match the order in which ports have been + * numbered by the HUB/HCD manufacturer. + * + * \param dev a device + * \returns the port number (0 if not available) + */ +uint8_t API_EXPORTED libusb_get_port_number(libusb_device *dev) +{ + return dev->port_number; +} + +/** \ingroup libusb_dev + * Get the list of all port numbers from root for the specified device + * + * Since version 1.0.16, \ref LIBUSBX_API_VERSION >= 0x01000102 + * \param dev a device + * \param port_numbers the array that should contain the port numbers + * \param port_numbers_len the maximum length of the array. As per the USB 3.0 + * specs, the current maximum limit for the depth is 7. + * \returns the number of elements filled + * \returns \ref LIBUSB_ERROR_OVERFLOW if the array is too small + */ +int API_EXPORTED libusb_get_port_numbers(libusb_device *dev, + uint8_t *port_numbers, int port_numbers_len) +{ + int i = port_numbers_len; + struct libusb_context *ctx = DEVICE_CTX(dev); + + if (port_numbers_len <= 0) + return LIBUSB_ERROR_INVALID_PARAM; + + /* HCDs can be listed as devices with port #0 */ + while((dev) && (dev->port_number != 0)) { + if (--i < 0) { + usbi_warn(ctx, "port numbers array is too small"); + return LIBUSB_ERROR_OVERFLOW; + } + port_numbers[i] = dev->port_number; + dev = dev->parent_dev; + } + if (i < port_numbers_len) + memmove(port_numbers, &port_numbers[i], (size_t)(port_numbers_len - i)); + return port_numbers_len - i; +} + +/** \ingroup libusb_dev + * \deprecated Please use \ref libusb_get_port_numbers() instead. + */ +int API_EXPORTED libusb_get_port_path(libusb_context *ctx, libusb_device *dev, + uint8_t *port_numbers, uint8_t port_numbers_len) +{ + UNUSED(ctx); + + return libusb_get_port_numbers(dev, port_numbers, port_numbers_len); +} + +/** \ingroup libusb_dev + * Get the the parent from the specified device. + * \param dev a device + * \returns the device parent or NULL if not available + * You should issue a \ref libusb_get_device_list() before calling this + * function and make sure that you only access the parent before issuing + * \ref libusb_free_device_list(). The reason is that libusb currently does + * not maintain a permanent list of device instances, and therefore can + * only guarantee that parents are fully instantiated within a + * libusb_get_device_list() - libusb_free_device_list() block. + */ +DEFAULT_VISIBILITY +libusb_device * LIBUSB_CALL libusb_get_parent(libusb_device *dev) +{ + return dev->parent_dev; +} + +/** \ingroup libusb_dev + * Get the address of the device on the bus it is connected to. + * \param dev a device + * \returns the device address + */ +uint8_t API_EXPORTED libusb_get_device_address(libusb_device *dev) +{ + return dev->device_address; +} + +/** \ingroup libusb_dev + * Get the negotiated connection speed for a device. + * \param dev a device + * \returns a \ref libusb_speed code, where LIBUSB_SPEED_UNKNOWN means that + * the OS doesn't know or doesn't support returning the negotiated speed. + */ +int API_EXPORTED libusb_get_device_speed(libusb_device *dev) +{ + return (int)(dev->speed); +} + +static const struct libusb_endpoint_descriptor *find_endpoint( + struct libusb_config_descriptor *config, unsigned char endpoint) +{ + int iface_idx; + for (iface_idx = 0; iface_idx < config->bNumInterfaces; iface_idx++) { + const struct libusb_interface *iface = &config->interface[iface_idx]; + int altsetting_idx; + + for (altsetting_idx = 0; altsetting_idx < iface->num_altsetting; + altsetting_idx++) { + const struct libusb_interface_descriptor *altsetting + = &iface->altsetting[altsetting_idx]; + int ep_idx; + + for (ep_idx = 0; ep_idx < altsetting->bNumEndpoints; ep_idx++) { + const struct libusb_endpoint_descriptor *ep = + &altsetting->endpoint[ep_idx]; + if (ep->bEndpointAddress == endpoint) + return ep; + } + } + } + return NULL; +} + +/** \ingroup libusb_dev + * Convenience function to retrieve the wMaxPacketSize value for a particular + * endpoint in the active device configuration. + * + * This function was originally intended to be of assistance when setting up + * isochronous transfers, but a design mistake resulted in this function + * instead. It simply returns the wMaxPacketSize value without considering + * its contents. If you're dealing with isochronous transfers, you probably + * want libusb_get_max_iso_packet_size() instead. + * + * \param dev a device + * \param endpoint address of the endpoint in question + * \returns the wMaxPacketSize value + * \returns \ref LIBUSB_ERROR_NOT_FOUND if the endpoint does not exist + * \returns \ref LIBUSB_ERROR_OTHER on other failure + */ +int API_EXPORTED libusb_get_max_packet_size(libusb_device *dev, + unsigned char endpoint) +{ + struct libusb_config_descriptor *config; + const struct libusb_endpoint_descriptor *ep; + int r; + + r = libusb_get_active_config_descriptor(dev, &config); + if (r < 0) { + usbi_err(DEVICE_CTX(dev), + "could not retrieve active config descriptor"); + return LIBUSB_ERROR_OTHER; + } + + ep = find_endpoint(config, endpoint); + if (!ep) { + r = LIBUSB_ERROR_NOT_FOUND; + goto out; + } + + r = ep->wMaxPacketSize; + +out: + libusb_free_config_descriptor(config); + return r; +} + +static const struct libusb_endpoint_descriptor *find_alt_endpoint( + struct libusb_config_descriptor *config, + int iface_idx, int altsetting_idx, unsigned char endpoint) +{ + if (iface_idx >= config->bNumInterfaces) { + return NULL; + } + + const struct libusb_interface *iface = &config->interface[iface_idx]; + + if (altsetting_idx >= iface->num_altsetting) { + return NULL; + } + + const struct libusb_interface_descriptor *altsetting + = &iface->altsetting[altsetting_idx]; + int ep_idx; + + for (ep_idx = 0; ep_idx < altsetting->bNumEndpoints; ep_idx++) { + const struct libusb_endpoint_descriptor *ep = + &altsetting->endpoint[ep_idx]; + if (ep->bEndpointAddress == endpoint) + return ep; + } + return NULL; +} + +static int get_endpoint_max_packet_size(libusb_device *dev, + const struct libusb_endpoint_descriptor *ep) +{ + struct libusb_ss_endpoint_companion_descriptor *ss_ep_cmp; + enum libusb_endpoint_transfer_type ep_type; + uint16_t val; + int r = 0; + int speed; + + speed = libusb_get_device_speed(dev); + if (speed >= LIBUSB_SPEED_SUPER) { + r = libusb_get_ss_endpoint_companion_descriptor(dev->ctx, ep, &ss_ep_cmp); + if (r == LIBUSB_SUCCESS) { + r = ss_ep_cmp->wBytesPerInterval; + libusb_free_ss_endpoint_companion_descriptor(ss_ep_cmp); + } + } + + /* If the device isn't a SuperSpeed device or retrieving the SS endpoint didn't worked. */ + if (speed < LIBUSB_SPEED_SUPER || r < 0) { + val = ep->wMaxPacketSize; + ep_type = (enum libusb_endpoint_transfer_type) (ep->bmAttributes & 0x3); + + r = val & 0x07ff; + if (ep_type == LIBUSB_ENDPOINT_TRANSFER_TYPE_ISOCHRONOUS + || ep_type == LIBUSB_ENDPOINT_TRANSFER_TYPE_INTERRUPT) + r *= (1 + ((val >> 11) & 3)); + } + + return r; +} + +/** \ingroup libusb_dev + * Calculate the maximum packet size which a specific endpoint is capable is + * sending or receiving in the duration of 1 microframe + * + * Only the active configuration is examined. The calculation is based on the + * wMaxPacketSize field in the endpoint descriptor as described in section + * 9.6.6 in the USB 2.0 specifications. + * + * If acting on an isochronous or interrupt endpoint, this function will + * multiply the value found in bits 0:10 by the number of transactions per + * microframe (determined by bits 11:12). Otherwise, this function just + * returns the numeric value found in bits 0:10. For USB 3.0 device, it + * will attempts to retrieve the Endpoint Companion Descriptor to return + * wBytesPerInterval. + * + * This function is useful for setting up isochronous transfers, for example + * you might pass the return value from this function to + * libusb_set_iso_packet_lengths() in order to set the length field of every + * isochronous packet in a transfer. + * + * This function only considers the first alternate setting of the interface. + * If the endpoint has different maximum packet sizes for different alternate + * settings, you probably want libusb_get_max_alt_packet_size() instead. + * + * Since v1.0.3. + * + * \param dev a device + * \param endpoint address of the endpoint in question + * \returns the maximum packet size which can be sent/received on this endpoint + * \returns \ref LIBUSB_ERROR_NOT_FOUND if the endpoint does not exist + * \returns \ref LIBUSB_ERROR_OTHER on other failure + * \see libusb_get_max_alt_packet_size + */ +int API_EXPORTED libusb_get_max_iso_packet_size(libusb_device *dev, + unsigned char endpoint) +{ + struct libusb_config_descriptor *config; + const struct libusb_endpoint_descriptor *ep; + int r; + + r = libusb_get_active_config_descriptor(dev, &config); + if (r < 0) { + usbi_err(DEVICE_CTX(dev), + "could not retrieve active config descriptor"); + return LIBUSB_ERROR_OTHER; + } + + ep = find_endpoint(config, endpoint); + if (!ep) { + r = LIBUSB_ERROR_NOT_FOUND; + goto out; + } + + r = get_endpoint_max_packet_size(dev, ep); + +out: + libusb_free_config_descriptor(config); + return r; +} + +/** \ingroup libusb_dev + * Calculate the maximum packet size which a specific endpoint is capable of + * sending or receiving in the duration of 1 microframe + * + * Only the active configuration is examined. The calculation is based on the + * wMaxPacketSize field in the endpoint descriptor as described in section + * 9.6.6 in the USB 2.0 specifications. + * + * If acting on an isochronous or interrupt endpoint, this function will + * multiply the value found in bits 0:10 by the number of transactions per + * microframe (determined by bits 11:12). Otherwise, this function just + * returns the numeric value found in bits 0:10. For USB 3.0 device, it + * will attempts to retrieve the Endpoint Companion Descriptor to return + * wBytesPerInterval. + * + * This function is useful for setting up isochronous transfers, for example + * you might pass the return value from this function to + * libusb_set_iso_packet_lengths() in order to set the length field of every + * isochronous packet in a transfer. + * + * Since version 1.0.27, \ref LIBUSB_API_VERSION >= 0x0100010A + * + * \param dev a device + * \param interface_number the bInterfaceNumber of the interface + * the endpoint belongs to + * \param alternate_setting the bAlternateSetting of the interface + * \param endpoint address of the endpoint in question + * \returns the maximum packet size which can be sent/received on this endpoint + * \returns \ref LIBUSB_ERROR_NOT_FOUND if the endpoint does not exist + * \returns \ref LIBUSB_ERROR_OTHER on other failure + * \see libusb_get_max_iso_packet_size + */ +int API_EXPORTED libusb_get_max_alt_packet_size(libusb_device *dev, + int interface_number, int alternate_setting, unsigned char endpoint) +{ + struct libusb_config_descriptor *config; + const struct libusb_endpoint_descriptor *ep; + int r; + + r = libusb_get_active_config_descriptor(dev, &config); + if (r < 0) { + usbi_err(DEVICE_CTX(dev), + "could not retrieve active config descriptor"); + return LIBUSB_ERROR_OTHER; + } + + ep = find_alt_endpoint(config, interface_number, + alternate_setting, endpoint); + if (!ep) { + r = LIBUSB_ERROR_NOT_FOUND; + goto out; + } + + r = get_endpoint_max_packet_size(dev, ep); + +out: + libusb_free_config_descriptor(config); + return r; +} + +/** \ingroup libusb_dev + * Increment the reference count of a device. + * \param dev the device to reference + * \returns the same device + */ +DEFAULT_VISIBILITY +libusb_device * LIBUSB_CALL libusb_ref_device(libusb_device *dev) +{ + long refcnt; + + refcnt = usbi_atomic_inc(&dev->refcnt); + assert(refcnt >= 2); + UNUSED(refcnt); + + return dev; +} + +/** \ingroup libusb_dev + * Decrement the reference count of a device. If the decrement operation + * causes the reference count to reach zero, the device shall be destroyed. + * \param dev the device to unreference + */ +void API_EXPORTED libusb_unref_device(libusb_device *dev) +{ + long refcnt; + + if (!dev) + return; + + if (!libusb_has_capability(LIBUSB_CAP_HAS_HOTPLUG)) { + /* Non-hotplug path: the decrement and conditional list removal + * must be atomic with respect to usbi_get_device_by_session_id(), + * which searches the list and refs the device under the same lock. + * Without this, a concurrent search could find a device whose + * refcnt has already reached zero and hit the assert in + * libusb_ref_device(). */ + struct libusb_context *ctx = DEVICE_CTX(dev); + + usbi_mutex_lock(&ctx->usb_devs_lock); + refcnt = usbi_atomic_dec(&dev->refcnt); + assert(refcnt >= 0); + if (refcnt == 0 && usbi_atomic_load(&dev->attached)) { + list_del(&dev->list); + usbi_atomic_store(&dev->attached, 0); + } + usbi_mutex_unlock(&ctx->usb_devs_lock); + } else { + refcnt = usbi_atomic_dec(&dev->refcnt); + assert(refcnt >= 0); + } + + if (refcnt == 0) { + usbi_dbg(DEVICE_CTX(dev), "destroy device %d.%d", dev->bus_number, dev->device_address); + + libusb_unref_device(dev->parent_dev); + + if (usbi_backend.destroy_device) + usbi_backend.destroy_device(dev); + + for (int idx = 0; idx < LIBUSB_DEVICE_STRING_COUNT; ++idx) { + free(dev->device_strings_utf8[idx]); + } + + free(dev); + } +} + +/** \ingroup libusb_dev + * Wrap a platform-specific system device handle and obtain a libusb device + * handle for the underlying device. The handle allows you to use libusb to + * perform I/O on the device in question. + * + * Call libusb_init_context with the LIBUSB_OPTION_NO_DEVICE_DISCOVERY + * option if you want to skip enumeration of USB devices. In particular, this + * might be needed on Android if you don't have authority to access USB + * devices in general. Setting this option with libusb_set_option is deprecated. + * + * On Linux, the system device handle must be a valid file descriptor opened + * on the device node. + * + * The system device handle must remain open until libusb_close() is called. + * The system device handle will not be closed by libusb_close(). + * + * Internally, this function creates a temporary device and makes it + * available to you through libusb_get_device(). This device is destroyed + * during libusb_close(). The device shall not be opened through libusb_open(). + * + * This is a non-blocking function; no requests are sent over the bus. + * + * Since version 1.0.23, \ref LIBUSB_API_VERSION >= 0x01000107 + * + * \param ctx the context to operate on, or NULL for the default context + * \param sys_dev the platform-specific system device handle + * \param dev_handle output location for the returned device handle pointer. Only + * populated when the return code is 0. + * \returns 0 on success + * \returns \ref LIBUSB_ERROR_NO_MEM on memory allocation failure + * \returns \ref LIBUSB_ERROR_ACCESS if the user has insufficient permissions + * \returns \ref LIBUSB_ERROR_NOT_SUPPORTED if the operation is not supported on this + * platform + * \returns another LIBUSB_ERROR code on other failure + */ +int API_EXPORTED libusb_wrap_sys_device(libusb_context *ctx, intptr_t sys_dev, + libusb_device_handle **dev_handle) +{ + struct libusb_device_handle *_dev_handle; + size_t priv_size = usbi_backend.device_handle_priv_size; + int r; + + usbi_dbg(ctx, "wrap_sys_device 0x%" PRIxPTR, (uintptr_t)sys_dev); + + ctx = usbi_get_context(ctx); + + if (!usbi_backend.wrap_sys_device) + return LIBUSB_ERROR_NOT_SUPPORTED; + + _dev_handle = calloc(1, PTR_ALIGN(sizeof(*_dev_handle)) + priv_size); + if (!_dev_handle) + return LIBUSB_ERROR_NO_MEM; + + usbi_mutex_init(&_dev_handle->lock); + + r = usbi_backend.wrap_sys_device(ctx, _dev_handle, sys_dev); + if (r < 0) { + usbi_dbg(ctx, "wrap_sys_device 0x%" PRIxPTR " returns %d", (uintptr_t)sys_dev, r); + usbi_mutex_destroy(&_dev_handle->lock); + free(_dev_handle); + return r; + } + + usbi_mutex_lock(&ctx->open_devs_lock); + list_add(&_dev_handle->list, &ctx->open_devs); + usbi_mutex_unlock(&ctx->open_devs_lock); + *dev_handle = _dev_handle; + + return 0; +} + +/** \ingroup libusb_dev + * Open a device and obtain a device handle. A handle allows you to perform + * I/O on the device in question. + * + * Internally, this function adds a reference to the device and makes it + * available to you through libusb_get_device(). This reference is removed + * during libusb_close(). + * + * This is a non-blocking function; no requests are sent over the bus. + * + * \param dev the device to open + * \param dev_handle output location for the returned device handle pointer. Only + * populated when the return code is 0. + * \returns 0 on success + * \returns \ref LIBUSB_ERROR_NO_MEM on memory allocation failure + * \returns \ref LIBUSB_ERROR_ACCESS if the user has insufficient permissions + * \returns \ref LIBUSB_ERROR_NO_DEVICE if the device has been disconnected + * \returns another LIBUSB_ERROR code on other failure + */ +int API_EXPORTED libusb_open(libusb_device *dev, + libusb_device_handle **dev_handle) +{ + struct libusb_context *ctx = DEVICE_CTX(dev); + struct libusb_device_handle *_dev_handle; + size_t priv_size = usbi_backend.device_handle_priv_size; + int r; + + usbi_dbg(DEVICE_CTX(dev), "open %d.%d", dev->bus_number, dev->device_address); + + if (!usbi_atomic_load(&dev->attached)) + return LIBUSB_ERROR_NO_DEVICE; + + _dev_handle = calloc(1, PTR_ALIGN(sizeof(*_dev_handle)) + priv_size); + if (!_dev_handle) + return LIBUSB_ERROR_NO_MEM; + + usbi_mutex_init(&_dev_handle->lock); + + _dev_handle->dev = libusb_ref_device(dev); + + r = usbi_backend.open(_dev_handle); + if (r < 0) { + usbi_dbg(DEVICE_CTX(dev), "open %d.%d returns %d", dev->bus_number, dev->device_address, r); + libusb_unref_device(dev); + usbi_mutex_destroy(&_dev_handle->lock); + free(_dev_handle); + return r; + } + + usbi_mutex_lock(&ctx->open_devs_lock); + list_add(&_dev_handle->list, &ctx->open_devs); + usbi_mutex_unlock(&ctx->open_devs_lock); + *dev_handle = _dev_handle; + + return 0; +} + +/** \ingroup libusb_dev + * Convenience function for finding a device with a particular + * idVendor/idProduct combination. This function is intended + * for those scenarios where you are using libusb to knock up a quick test + * application - it allows you to avoid calling libusb_get_device_list() and + * worrying about traversing/freeing the list. + * + * This function has limitations and is hence not intended for use in real + * applications: if multiple devices have the same IDs it will only + * give you the first one, etc. + * + * \param ctx the context to operate on, or NULL for the default context + * \param vendor_id the idVendor value to search for + * \param product_id the idProduct value to search for + * \returns a device handle for the first found device, or NULL on error + * or if the device could not be found. */ +DEFAULT_VISIBILITY +libusb_device_handle * LIBUSB_CALL libusb_open_device_with_vid_pid( + libusb_context *ctx, uint16_t vendor_id, uint16_t product_id) +{ + struct libusb_device **devs; + struct libusb_device *found = NULL; + struct libusb_device *dev; + struct libusb_device_handle *dev_handle = NULL; + size_t i = 0; + int r; + + if (libusb_get_device_list(ctx, &devs) < 0) + return NULL; + + while ((dev = devs[i++]) != NULL) { + struct libusb_device_descriptor desc; + r = libusb_get_device_descriptor(dev, &desc); + if (r < 0) + goto out; + if (desc.idVendor == vendor_id && desc.idProduct == product_id) { + found = dev; + break; + } + } + + if (found) { + r = libusb_open(found, &dev_handle); + if (r < 0) + dev_handle = NULL; + } + +out: + libusb_free_device_list(devs, 1); + return dev_handle; +} + +static void do_close(struct libusb_context *ctx, + struct libusb_device_handle *dev_handle) +{ + struct usbi_transfer *itransfer; + struct usbi_transfer *tmp; + + /* remove any transfers in flight that are for this device */ + usbi_mutex_lock(&ctx->flying_transfers_lock); + + /* safe iteration because transfers may be being deleted */ + for_each_transfer_safe(ctx, itransfer, tmp) { + struct libusb_transfer *transfer = + USBI_TRANSFER_TO_LIBUSB_TRANSFER(itransfer); + uint32_t state_flags; + + if (transfer->dev_handle != dev_handle) + continue; + + usbi_mutex_lock(&itransfer->lock); + state_flags = itransfer->state_flags; + usbi_mutex_unlock(&itransfer->lock); + if (!(state_flags & USBI_TRANSFER_DEVICE_DISAPPEARED)) { + usbi_err(ctx, "Device handle closed while transfer was still being processed, but the device is still connected as far as we know"); + + if (state_flags & USBI_TRANSFER_CANCELLING) + usbi_warn(ctx, "A cancellation for an in-flight transfer hasn't completed but closing the device handle"); + else + usbi_err(ctx, "A cancellation hasn't even been scheduled on the transfer for which the device is closing"); + } + + /* remove from the list of in-flight transfers and make sure + * we don't accidentally use the device handle in the future + * (or that such accesses will be easily caught and identified as a crash) + */ + list_del(&itransfer->list); + transfer->dev_handle = NULL; + + /* it is up to the user to free up the actual transfer struct. this is + * just making sure that we don't attempt to process the transfer after + * the device handle is invalid + */ + usbi_dbg(ctx, "Removed transfer %p from the in-flight list because device handle %p closed", + (void *) transfer, (void *) dev_handle); + } + usbi_mutex_unlock(&ctx->flying_transfers_lock); + + usbi_mutex_lock(&ctx->open_devs_lock); + list_del(&dev_handle->list); + usbi_mutex_unlock(&ctx->open_devs_lock); + + usbi_backend.close(dev_handle); + libusb_unref_device(dev_handle->dev); + usbi_mutex_destroy(&dev_handle->lock); + free(dev_handle); +} + +/** \ingroup libusb_dev + * Close a device handle. Should be called on all open handles before your + * application exits. + * + * Internally, this function destroys the reference that was added by + * libusb_open() on the given device. + * + * This is a non-blocking function; no requests are sent over the bus. + * + * \param dev_handle the device handle to close + */ +void API_EXPORTED libusb_close(libusb_device_handle *dev_handle) +{ + struct libusb_context *ctx; + unsigned int event_flags; + int handling_events; + + if (!dev_handle) + return; + ctx = HANDLE_CTX(dev_handle); + usbi_dbg(ctx, " "); + + handling_events = usbi_handling_events(ctx); + + /* Similarly to libusb_open(), we want to interrupt all event handlers + * at this point. More importantly, we want to perform the actual close of + * the device while holding the event handling lock (preventing any other + * thread from doing event handling) because we will be removing a file + * descriptor from the polling loop. If this is being called by the current + * event handler, we can bypass the interruption code because we already + * hold the event handling lock. */ + + if (!handling_events) { + /* Record that we are closing a device. + * Only signal an event if there are no prior pending events. */ + usbi_mutex_lock(&ctx->event_data_lock); + event_flags = ctx->event_flags; + if (!ctx->device_close++) + ctx->event_flags |= USBI_EVENT_DEVICE_CLOSE; + if (!event_flags) + usbi_signal_event(&ctx->event); + usbi_mutex_unlock(&ctx->event_data_lock); + + /* take event handling lock */ + libusb_lock_events(ctx); + } + + /* Close the device */ + do_close(ctx, dev_handle); + + if (!handling_events) { + /* We're done with closing this device. + * Clear the event pipe if there are no further pending events. */ + usbi_mutex_lock(&ctx->event_data_lock); + if (!--ctx->device_close) + ctx->event_flags &= ~USBI_EVENT_DEVICE_CLOSE; + if (!ctx->event_flags) + usbi_clear_event(&ctx->event); + usbi_mutex_unlock(&ctx->event_data_lock); + + /* Release event handling lock and wake up event waiters */ + libusb_unlock_events(ctx); + } +} + +/** \ingroup libusb_dev + * Get the underlying device for a device handle. This function does not modify + * the reference count of the returned device, so do not feel compelled to + * unreference it when you are done. + * \param dev_handle a device handle + * \returns the underlying device + */ +DEFAULT_VISIBILITY +libusb_device * LIBUSB_CALL libusb_get_device(libusb_device_handle *dev_handle) +{ + return dev_handle->dev; +} + +/** \ingroup libusb_dev + * Determine the bConfigurationValue of the currently active configuration. + * + * You could formulate your own control request to obtain this information, + * but this function has the advantage that it may be able to retrieve the + * information from operating system caches (no I/O involved). + * + * If the OS does not cache this information, then this function will block + * while a control transfer is submitted to retrieve the information. + * + * This function will return a value of 0 in the config output + * parameter if the device is in unconfigured state. + * + * \param dev_handle a device handle + * \param config output location for the bConfigurationValue of the active + * configuration (only valid for return code 0) + * \returns 0 on success + * \returns \ref LIBUSB_ERROR_NO_DEVICE if the device has been disconnected + * \returns another LIBUSB_ERROR code on other failure + */ +int API_EXPORTED libusb_get_configuration(libusb_device_handle *dev_handle, + int *config) +{ + int r = LIBUSB_ERROR_NOT_SUPPORTED; + uint8_t tmp = 0; + struct libusb_context *ctx = HANDLE_CTX(dev_handle); + + usbi_dbg(ctx, " "); + if (usbi_backend.get_configuration) + r = usbi_backend.get_configuration(dev_handle, &tmp); + + if (r == LIBUSB_ERROR_NOT_SUPPORTED) { + usbi_dbg(ctx, "falling back to control message"); + r = libusb_control_transfer(dev_handle, LIBUSB_ENDPOINT_IN, + LIBUSB_REQUEST_GET_CONFIGURATION, 0, 0, &tmp, 1, 1000); + if (r == 1) { + r = 0; + } else if (r == 0) { + usbi_err(ctx, "zero bytes returned in ctrl transfer?"); + r = LIBUSB_ERROR_IO; + } else { + usbi_dbg(ctx, "control failed, error %d", r); + } + } + + if (r == 0) { + usbi_dbg(ctx, "active config %u", tmp); + *config = (int)tmp; + } + + return r; +} + +/** \ingroup libusb_dev + * Set the active configuration for a device. + * + * The operating system may or may not have already set an active + * configuration on the device. It is up to your application to ensure the + * correct configuration is selected before you attempt to claim interfaces + * and perform other operations. + * + * If you call this function on a device already configured with the selected + * configuration, then this function will act as a lightweight device reset: + * it will issue a SET_CONFIGURATION request using the current configuration, + * causing most USB-related device state to be reset (altsetting reset to zero, + * endpoint halts cleared, toggles reset). + * + * Not all backends support setting the configuration from user space, which + * will be indicated by the return code \ref LIBUSB_ERROR_NOT_SUPPORTED. As this + * suggests that the platform is handling the device configuration itself, + * this error should generally be safe to ignore. + * + * You cannot change/reset configuration if your application has claimed + * interfaces. It is advised to set the desired configuration before claiming + * interfaces. + * + * Alternatively you can call libusb_release_interface() first. Note if you + * do things this way you must ensure that auto_detach_kernel_driver for + * dev is 0, otherwise the kernel driver will be re-attached when you + * release the interface(s). + * + * You cannot change/reset configuration if other applications or drivers have + * claimed interfaces. + * + * A configuration value of -1 will put the device in unconfigured state. + * The USB specifications state that a configuration value of 0 does this, + * however buggy devices exist which actually have a configuration 0. + * + * You should always use this function rather than formulating your own + * SET_CONFIGURATION control request. This is because the underlying operating + * system needs to know when such changes happen. + * + * This is a blocking function. + * + * \param dev_handle a device handle + * \param configuration the bConfigurationValue of the configuration you + * wish to activate, or -1 if you wish to put the device in an unconfigured + * state + * \returns 0 on success + * \returns \ref LIBUSB_ERROR_NOT_FOUND if the requested configuration does not exist + * \returns \ref LIBUSB_ERROR_BUSY if interfaces are currently claimed + * \returns \ref LIBUSB_ERROR_NOT_SUPPORTED if setting or changing the configuration + * is not supported by the backend + * \returns \ref LIBUSB_ERROR_NO_DEVICE if the device has been disconnected + * \returns another LIBUSB_ERROR code on other failure + * \see libusb_set_auto_detach_kernel_driver() + */ +int API_EXPORTED libusb_set_configuration(libusb_device_handle *dev_handle, + int configuration) +{ + usbi_dbg(HANDLE_CTX(dev_handle), "configuration %d", configuration); + if (configuration < -1 || configuration > (int)UINT8_MAX) + return LIBUSB_ERROR_INVALID_PARAM; + return usbi_backend.set_configuration(dev_handle, configuration); +} + +/** \ingroup libusb_dev + * Claim an interface on a given device handle. You must claim the interface + * you wish to use before you can perform I/O on any of its endpoints. + * + * It is legal to attempt to claim an already-claimed interface, in which + * case libusb just returns 0 without doing anything. + * + * If auto_detach_kernel_driver is set to 1 for dev, the kernel driver + * will be detached if necessary, on failure the detach error is returned. + * + * Claiming of interfaces is a purely logical operation; it does not cause + * any requests to be sent over the bus. Interface claiming is used to + * instruct the underlying operating system that your application wishes + * to take ownership of the interface. + * + * This is a non-blocking function. + * + * \param dev_handle a device handle + * \param interface_number the bInterfaceNumber of the interface you + * wish to claim + * \returns 0 on success + * \returns \ref LIBUSB_ERROR_NOT_FOUND if the requested interface does not exist + * \returns \ref LIBUSB_ERROR_BUSY if another program or driver has claimed the + * interface + * \returns \ref LIBUSB_ERROR_NO_DEVICE if the device has been disconnected + * \returns a LIBUSB_ERROR code on other failure + * \see libusb_set_auto_detach_kernel_driver() + */ +int API_EXPORTED libusb_claim_interface(libusb_device_handle *dev_handle, + int interface_number) +{ + int r = 0; + + usbi_dbg(HANDLE_CTX(dev_handle), "interface %d", interface_number); + if (interface_number < 0 || interface_number >= USB_MAXINTERFACES) + return LIBUSB_ERROR_INVALID_PARAM; + + if (!usbi_atomic_load(&dev_handle->dev->attached)) + return LIBUSB_ERROR_NO_DEVICE; + + usbi_mutex_lock(&dev_handle->lock); + if (dev_handle->claimed_interfaces & (1U << interface_number)) + goto out; + + r = usbi_backend.claim_interface(dev_handle, (uint8_t)interface_number); + if (r == 0) + dev_handle->claimed_interfaces |= 1U << interface_number; + +out: + usbi_mutex_unlock(&dev_handle->lock); + return r; +} + +/** \ingroup libusb_dev + * Release an interface previously claimed with libusb_claim_interface(). You + * should release all claimed interfaces before closing a device handle. + * + * This is a blocking function. A SET_INTERFACE control request will be sent + * to the device, resetting interface state to the first alternate setting. + * + * If auto_detach_kernel_driver is set to 1 for dev, the kernel + * driver will be re-attached after releasing the interface. + * + * \param dev_handle a device handle + * \param interface_number the bInterfaceNumber of the + * previously-claimed interface + * \returns 0 on success + * \returns \ref LIBUSB_ERROR_NOT_FOUND if the interface was not claimed + * \returns \ref LIBUSB_ERROR_NO_DEVICE if the device has been disconnected + * \returns another LIBUSB_ERROR code on other failure + * \see libusb_set_auto_detach_kernel_driver() + */ +int API_EXPORTED libusb_release_interface(libusb_device_handle *dev_handle, + int interface_number) +{ + int r; + + usbi_dbg(HANDLE_CTX(dev_handle), "interface %d", interface_number); + if (interface_number < 0 || interface_number >= USB_MAXINTERFACES) + return LIBUSB_ERROR_INVALID_PARAM; + + usbi_mutex_lock(&dev_handle->lock); + if (!(dev_handle->claimed_interfaces & (1U << interface_number))) { + r = LIBUSB_ERROR_NOT_FOUND; + goto out; + } + + r = usbi_backend.release_interface(dev_handle, (uint8_t)interface_number); + if (r == 0) + dev_handle->claimed_interfaces &= ~(1U << interface_number); + +out: + usbi_mutex_unlock(&dev_handle->lock); + return r; +} + +/** \ingroup libusb_dev + * Activate an alternate setting for an interface. The interface must have + * been previously claimed with libusb_claim_interface(). + * + * You should always use this function rather than formulating your own + * SET_INTERFACE control request. This is because the underlying operating + * system needs to know when such changes happen. + * + * This is a blocking function. + * + * \param dev_handle a device handle + * \param interface_number the bInterfaceNumber of the + * previously-claimed interface + * \param alternate_setting the bAlternateSetting of the alternate + * setting to activate + * \returns 0 on success + * \returns \ref LIBUSB_ERROR_NOT_FOUND if the interface was not claimed, or the + * requested alternate setting does not exist + * \returns \ref LIBUSB_ERROR_NO_DEVICE if the device has been disconnected + * \returns another LIBUSB_ERROR code on other failure + */ +int API_EXPORTED libusb_set_interface_alt_setting(libusb_device_handle *dev_handle, + int interface_number, int alternate_setting) +{ + usbi_dbg(HANDLE_CTX(dev_handle), "interface %d altsetting %d", + interface_number, alternate_setting); + if (interface_number < 0 || interface_number >= USB_MAXINTERFACES) + return LIBUSB_ERROR_INVALID_PARAM; + if (alternate_setting < 0 || alternate_setting > (int)UINT8_MAX) + return LIBUSB_ERROR_INVALID_PARAM; + + if (!usbi_atomic_load(&dev_handle->dev->attached)) { + return LIBUSB_ERROR_NO_DEVICE; + } + + usbi_mutex_lock(&dev_handle->lock); + if (!(dev_handle->claimed_interfaces & (1U << interface_number))) { + usbi_mutex_unlock(&dev_handle->lock); + return LIBUSB_ERROR_NOT_FOUND; + } + usbi_mutex_unlock(&dev_handle->lock); + + return usbi_backend.set_interface_altsetting(dev_handle, + (uint8_t)interface_number, (uint8_t)alternate_setting); +} + +/** \ingroup libusb_dev + * Clear the halt/stall condition for an endpoint. Endpoints with halt status + * are unable to receive or transmit data until the halt condition is stalled. + * + * You should cancel all pending transfers before attempting to clear the halt + * condition. + * + * This is a blocking function. + * + * \param dev_handle a device handle + * \param endpoint the endpoint to clear halt status + * \returns 0 on success + * \returns \ref LIBUSB_ERROR_NOT_FOUND if the endpoint does not exist + * \returns \ref LIBUSB_ERROR_NO_DEVICE if the device has been disconnected + * \returns another LIBUSB_ERROR code on other failure + */ +int API_EXPORTED libusb_clear_halt(libusb_device_handle *dev_handle, + unsigned char endpoint) +{ + usbi_dbg(HANDLE_CTX(dev_handle), "endpoint 0x%x", endpoint); + if (!usbi_atomic_load(&dev_handle->dev->attached)) + return LIBUSB_ERROR_NO_DEVICE; + + return usbi_backend.clear_halt(dev_handle, endpoint); +} + +/** \ingroup libusb_dev + * Perform a USB port reset to reinitialize a device. The system will attempt + * to restore the previous configuration and alternate settings after the + * reset has completed. + * + * If the reset fails, the descriptors change, or the previous state cannot be + * restored, the device will appear to be disconnected and reconnected. This + * means that the device handle is no longer valid (you should close it) and + * rediscover the device. A return code of \ref LIBUSB_ERROR_NOT_FOUND indicates + * when this is the case. + * + * This is a blocking function which usually incurs a noticeable delay. + * + * \param dev_handle a handle of the device to reset + * \returns 0 on success + * \returns \ref LIBUSB_ERROR_NOT_FOUND if re-enumeration is required, or if the + * device has been disconnected + * \returns another LIBUSB_ERROR code on other failure + */ +int API_EXPORTED libusb_reset_device(libusb_device_handle *dev_handle) +{ + usbi_dbg(HANDLE_CTX(dev_handle), " "); + if (!usbi_atomic_load(&dev_handle->dev->attached)) + return LIBUSB_ERROR_NO_DEVICE; + + if (usbi_backend.reset_device) + return usbi_backend.reset_device(dev_handle); + else + return LIBUSB_ERROR_NOT_SUPPORTED; +} + +/** \ingroup libusb_asyncio + * Allocate up to num_streams usb bulk streams on the specified endpoints. This + * function takes an array of endpoints rather then a single endpoint because + * some protocols require that endpoints are setup with similar stream ids. + * All endpoints passed in must belong to the same interface. + * + * Note this function may return less streams then requested. Also note that the + * same number of streams are allocated for each endpoint in the endpoint array. + * + * Stream id 0 is reserved, and should not be used to communicate with devices. + * If libusb_alloc_streams() returns with a value of N, you may use stream ids + * 1 to N. + * + * Since version 1.0.19, \ref LIBUSB_API_VERSION >= 0x01000103 + * + * \param dev_handle a device handle + * \param num_streams number of streams to try to allocate + * \param endpoints array of endpoints to allocate streams on + * \param num_endpoints length of the endpoints array + * \returns number of streams allocated, or a LIBUSB_ERROR code on failure + */ +int API_EXPORTED libusb_alloc_streams(libusb_device_handle *dev_handle, + uint32_t num_streams, unsigned char *endpoints, int num_endpoints) +{ + usbi_dbg(HANDLE_CTX(dev_handle), "streams %u eps %d", (unsigned)num_streams, num_endpoints); + + if (!num_streams || !endpoints || num_endpoints <= 0) + return LIBUSB_ERROR_INVALID_PARAM; + + if (!usbi_atomic_load(&dev_handle->dev->attached)) + return LIBUSB_ERROR_NO_DEVICE; + + if (usbi_backend.alloc_streams) + return usbi_backend.alloc_streams(dev_handle, num_streams, endpoints, + num_endpoints); + else + return LIBUSB_ERROR_NOT_SUPPORTED; +} + +/** \ingroup libusb_asyncio + * Free usb bulk streams allocated with libusb_alloc_streams(). + * + * Note streams are automatically free-ed when releasing an interface. + * + * Since version 1.0.19, \ref LIBUSB_API_VERSION >= 0x01000103 + * + * \param dev_handle a device handle + * \param endpoints array of endpoints to free streams on + * \param num_endpoints length of the endpoints array + * \returns \ref LIBUSB_SUCCESS, or a LIBUSB_ERROR code on failure + */ +int API_EXPORTED libusb_free_streams(libusb_device_handle *dev_handle, + unsigned char *endpoints, int num_endpoints) +{ + usbi_dbg(HANDLE_CTX(dev_handle), "eps %d", num_endpoints); + + if (!endpoints || num_endpoints <= 0) + return LIBUSB_ERROR_INVALID_PARAM; + + if (!usbi_atomic_load(&dev_handle->dev->attached)) + return LIBUSB_ERROR_NO_DEVICE; + + if (usbi_backend.free_streams) + return usbi_backend.free_streams(dev_handle, endpoints, + num_endpoints); + else + return LIBUSB_ERROR_NOT_SUPPORTED; +} + +/** \ingroup libusb_asyncio + * Attempts to allocate a block of persistent DMA memory suitable for transfers + * against the given device. If successful, will return a block of memory + * that is suitable for use as "buffer" in \ref libusb_transfer against this + * device. Using this memory instead of regular memory means that the host + * controller can use DMA directly into the buffer to increase performance, and + * also that transfers can no longer fail due to kernel memory fragmentation. + * + * Note that this means you should not modify this memory (or even data on + * the same cache lines) when a transfer is in progress, although it is legal + * to have several transfers going on within the same memory block. + * + * Will return NULL on failure. Many systems do not support such zero-copy + * and will always return NULL. Memory allocated with this function must be + * freed with \ref libusb_dev_mem_free. Specifically, this means that the + * flag \ref LIBUSB_TRANSFER_FREE_BUFFER cannot be used to free memory allocated + * with this function. + * + * Since version 1.0.21, \ref LIBUSB_API_VERSION >= 0x01000105 + * + * \param dev_handle a device handle + * \param length size of desired data buffer + * \returns a pointer to the newly allocated memory, or NULL on failure + */ +DEFAULT_VISIBILITY +unsigned char * LIBUSB_CALL libusb_dev_mem_alloc(libusb_device_handle *dev_handle, + size_t length) +{ + if (!usbi_atomic_load(&dev_handle->dev->attached)) + return NULL; + + if (usbi_backend.dev_mem_alloc) + return usbi_backend.dev_mem_alloc(dev_handle, length); + else + return NULL; +} + +/** \ingroup libusb_asyncio + * Free device memory allocated with libusb_dev_mem_alloc(). + * + * \param dev_handle a device handle + * \param buffer pointer to the previously allocated memory + * \param length size of previously allocated memory + * \returns \ref LIBUSB_SUCCESS, or a LIBUSB_ERROR code on failure + */ +int API_EXPORTED libusb_dev_mem_free(libusb_device_handle *dev_handle, + unsigned char *buffer, size_t length) +{ + if (usbi_backend.dev_mem_free) + return usbi_backend.dev_mem_free(dev_handle, buffer, length); + else + return LIBUSB_ERROR_NOT_SUPPORTED; +} + +/** \ingroup libusb_dev + * Determine if a kernel driver is active on an interface. If a kernel driver + * is active, you cannot claim the interface, and libusb will be unable to + * perform I/O. + * + * This functionality is not available on Windows. + * + * \param dev_handle a device handle + * \param interface_number the interface to check + * \returns 0 if no kernel driver is active + * \returns 1 if a kernel driver is active + * \returns \ref LIBUSB_ERROR_NO_DEVICE if the device has been disconnected + * \returns \ref LIBUSB_ERROR_NOT_SUPPORTED on platforms where the functionality + * is not available + * \returns another LIBUSB_ERROR code on other failure + * \see libusb_detach_kernel_driver() + */ +int API_EXPORTED libusb_kernel_driver_active(libusb_device_handle *dev_handle, + int interface_number) +{ + usbi_dbg(HANDLE_CTX(dev_handle), "interface %d", interface_number); + + if (interface_number < 0 || interface_number >= USB_MAXINTERFACES) + return LIBUSB_ERROR_INVALID_PARAM; + + if (!usbi_atomic_load(&dev_handle->dev->attached)) + return LIBUSB_ERROR_NO_DEVICE; + + if (usbi_backend.kernel_driver_active) + return usbi_backend.kernel_driver_active(dev_handle, (uint8_t)interface_number); + else + return LIBUSB_ERROR_NOT_SUPPORTED; +} + +/** \ingroup libusb_dev + * Detach a kernel driver from an interface. If successful, you will then be + * able to claim the interface and perform I/O. + * + * This functionality is not available on Windows. + * + * Note that libusb itself also talks to the device through a special kernel + * driver, if this driver is already attached to the device, this call will + * not detach it and return \ref LIBUSB_ERROR_NOT_FOUND. + * + * \param dev_handle a device handle + * \param interface_number the interface to detach the driver from + * \returns 0 on success + * \returns \ref LIBUSB_ERROR_NOT_FOUND if no kernel driver was active + * \returns \ref LIBUSB_ERROR_INVALID_PARAM if the interface does not exist + * \returns \ref LIBUSB_ERROR_NO_DEVICE if the device has been disconnected + * \returns \ref LIBUSB_ERROR_NOT_SUPPORTED on platforms where the functionality + * is not available + * \returns another LIBUSB_ERROR code on other failure + * \see libusb_kernel_driver_active() + */ +int API_EXPORTED libusb_detach_kernel_driver(libusb_device_handle *dev_handle, + int interface_number) +{ + usbi_dbg(HANDLE_CTX(dev_handle), "interface %d", interface_number); + + if (interface_number < 0 || interface_number >= USB_MAXINTERFACES) + return LIBUSB_ERROR_INVALID_PARAM; + + if (!usbi_atomic_load(&dev_handle->dev->attached)) + return LIBUSB_ERROR_NO_DEVICE; + + if (usbi_backend.detach_kernel_driver) + return usbi_backend.detach_kernel_driver(dev_handle, (uint8_t)interface_number); + else + return LIBUSB_ERROR_NOT_SUPPORTED; +} + +/** \ingroup libusb_dev + * Re-attach an interface's kernel driver, which was previously detached + * using libusb_detach_kernel_driver(). + * + * This functionality is not available on Windows. + * + * \param dev_handle a device handle + * \param interface_number the interface to attach the driver from + * \returns 0 on success + * \returns \ref LIBUSB_ERROR_NOT_FOUND if no kernel driver was active + * \returns \ref LIBUSB_ERROR_INVALID_PARAM if the interface does not exist + * \returns \ref LIBUSB_ERROR_NO_DEVICE if the device has been disconnected + * \returns \ref LIBUSB_ERROR_NOT_SUPPORTED on platforms where the functionality + * is not available + * \returns \ref LIBUSB_ERROR_BUSY if the driver cannot be attached because the + * interface is claimed by a program or driver + * \returns another LIBUSB_ERROR code on other failure + * \see libusb_kernel_driver_active() + */ +int API_EXPORTED libusb_attach_kernel_driver(libusb_device_handle *dev_handle, + int interface_number) +{ + usbi_dbg(HANDLE_CTX(dev_handle), "interface %d", interface_number); + + if (interface_number < 0 || interface_number >= USB_MAXINTERFACES) + return LIBUSB_ERROR_INVALID_PARAM; + + if (!usbi_atomic_load(&dev_handle->dev->attached)) + return LIBUSB_ERROR_NO_DEVICE; + + if (usbi_backend.attach_kernel_driver) + return usbi_backend.attach_kernel_driver(dev_handle, (uint8_t)interface_number); + else + return LIBUSB_ERROR_NOT_SUPPORTED; +} + +/** \ingroup libusb_dev + * Enable/disable libusb's automatic kernel driver detachment. When this is + * enabled libusb will automatically detach the kernel driver on an interface + * when claiming the interface, and attach it when releasing the interface. + * + * Automatic kernel driver detachment is disabled on newly opened device + * handles by default. + * + * On platforms which do not have LIBUSB_CAP_SUPPORTS_DETACH_KERNEL_DRIVER + * this function will return \ref LIBUSB_ERROR_NOT_SUPPORTED, and libusb will + * continue as if this function was never called. + * + * \param dev_handle a device handle + * \param enable whether to enable or disable auto kernel driver detachment + * + * \returns \ref LIBUSB_SUCCESS on success + * \returns \ref LIBUSB_ERROR_NOT_SUPPORTED on platforms where the functionality + * is not available + * \see libusb_claim_interface() + * \see libusb_release_interface() + * \see libusb_set_configuration() + */ +int API_EXPORTED libusb_set_auto_detach_kernel_driver( + libusb_device_handle *dev_handle, int enable) +{ + if (!(usbi_backend.caps & USBI_CAP_SUPPORTS_DETACH_KERNEL_DRIVER)) + return LIBUSB_ERROR_NOT_SUPPORTED; + + dev_handle->auto_detach_kernel_driver = enable; + return LIBUSB_SUCCESS; +} + +/** \ingroup libusb_dev + * Check if the endpoint supports RAW_IO. + * \param dev_handle a device handle + * \param endpoint the endpoint to check + * + * \returns 1 if the endpoint supports RAW_IO + * \returns 0 if the endpoint does not support RAW_IO + * \returns a LIBUSB_ERROR code on failure + * + * Only endpoints using the WinUSB driver support RAW_IO, + * for all other backends/drivers this function will return 0. + * + * Fails if the interface the endpoint belongs to isn't claimed, + * \see libusb_claim_interface(). + * + * \see libusb_endpoint_set_raw_io() + * + * Since version 1.0.30, \ref LIBUSB_API_VERSION >= 0x0100010C + */ +int API_EXPORTED libusb_endpoint_supports_raw_io(libusb_device_handle* dev_handle, + uint8_t endpoint) +{ + if (usbi_backend.endpoint_supports_raw_io == NULL) + { + return 0; + } + + // If the `endpoint_supports_raw_io` function is present, these two should be too: + assert(usbi_backend.endpoint_set_raw_io != NULL); + assert(usbi_backend.get_max_raw_io_transfer_size != NULL); + + return usbi_backend.endpoint_supports_raw_io(dev_handle, endpoint); +} + +/** \ingroup libusb_dev + * Enable/disable RAW_IO for an endpoint on an open device. + * + * Only endpoints using the WinUSB driver support RAW_IO, + * for all other backends/drivers this function will return an error code. + * + * Using RAW_IO can greatly improve USB throughput by directly passing + * transfer requests to the underlying USB driver instead of queuing them + * in WinUSB. This can be particularly useful for high-throughput devices + * like cameras or oscilloscopes. + * + * Transfers submitted to the endpoint while RAW_IO is enabled will fail unless + * they adhere to the following rules : + * - The buffer length must be a multiple of the maximum endpoint packet size + * \see libusb_get_max_packet_size. + * - The buffer length must be less than or equal to the value returned by + * \ref libusb_get_max_raw_io_transfer_size. + * + * This option should not be changed when any transfer is in progress on + * the specified endpoint. + * + * Fails if the interface the endpoint belongs to isn't claimed, + * \see libusb_claim_interface(). + * + * \param dev_handle a device handle + * \param endpoint the endpoint to set RAW_IO for + * \param enable 1 to enable RAW_IO, 0 to disable it + * + * \returns \ref LIBUSB_SUCCESS on success + * \returns \ref LIBUSB_ERROR_NOT_SUPPORTED if the backend does not support RAW_IO. + * \returns another LIBUSB_ERROR code on other failure + * + * \see libusb_endpoint_supports_raw_io() + * \see https://learn.microsoft.com/en-us/windows-hardware/drivers/usbcon/winusb-functions-for-pipe-policy-modification + * + * Since version 1.0.30, \ref LIBUSB_API_VERSION >= 0x0100010C + */ +int API_EXPORTED libusb_endpoint_set_raw_io(libusb_device_handle* dev_handle, + uint8_t endpoint, int enable) +{ + if (!usbi_backend.endpoint_set_raw_io) + return LIBUSB_ERROR_NOT_SUPPORTED; + + return usbi_backend.endpoint_set_raw_io(dev_handle, endpoint, enable); +} + +/** \ingroup libusb_dev + * Retrieve the maximum transfer size in bytes supported for WinUSB RAW_IO + * for an inbound bulk or interrupt endpoint on an open device. + * + * Returns a maximum transfer size in bytes, or a negative error code. + * If the backend does not support WinUSB RAW_IO, returns + * \ref LIBUSB_ERROR_NOT_SUPPORTED. + * + * Fails if the interface the endpoint belongs to isn't claimed, + * \see libusb_claim_interface(). + * + * \see libusb_endpoint_set_raw_io() + * + * Since version 1.0.30, \ref LIBUSB_API_VERSION >= 0x0100010C + */ +int API_EXPORTED libusb_get_max_raw_io_transfer_size( + libusb_device_handle *dev_handle, uint8_t endpoint) +{ + if (!usbi_backend.get_max_raw_io_transfer_size) + return LIBUSB_ERROR_NOT_SUPPORTED; + + return usbi_backend.get_max_raw_io_transfer_size( + dev_handle, endpoint); +} + +/** \ingroup libusb_lib + * Deprecated. Use libusb_set_option() or libusb_init_context() instead, + * with the \ref LIBUSB_OPTION_LOG_LEVEL option. + */ +void API_EXPORTED libusb_set_debug(libusb_context *ctx, int level) +{ + libusb_set_option(ctx, LIBUSB_OPTION_LOG_LEVEL, level); +} + +static void libusb_set_log_cb_internal(libusb_context *ctx, libusb_log_cb cb, + int mode) +{ +#if defined(ENABLE_LOGGING) && (!defined(ENABLE_DEBUG_LOGGING) || !defined(USE_SYSTEM_LOGGING_FACILITY)) +#if !defined(USE_SYSTEM_LOGGING_FACILITY) + if (mode & LIBUSB_LOG_CB_GLOBAL) + log_handler = cb; +#endif +#if !defined(ENABLE_DEBUG_LOGGING) + if (mode & LIBUSB_LOG_CB_CONTEXT) { + ctx = usbi_get_context(ctx); + ctx->log_handler = cb; + } +#else + UNUSED(ctx); +#endif +#else + UNUSED(ctx); + UNUSED(cb); + UNUSED(mode); +#endif +} + +/** \ingroup libusb_lib + * Set log handler. + * + * libusb will redirect its log messages to the provided callback function. + * libusb supports redirection of per context and global log messages. + * Log messages sent to the context will be sent to the global log handler too. + * + * If libusb is compiled without message logging or USE_SYSTEM_LOGGING_FACILITY + * is defined then global callback function will never be called. + * If ENABLE_DEBUG_LOGGING is defined then per context callback function will + * never be called. + * + * Since version 1.0.23, \ref LIBUSB_API_VERSION >= 0x01000107 + * + * \param ctx context on which to assign log handler, or NULL for the default + * context. Parameter ignored if only LIBUSB_LOG_CB_GLOBAL mode is requested. + * \param cb pointer to the callback function, or NULL to stop log + * messages redirection + * \param mode mode of callback function operation. Several modes can be + * selected for a single callback function, see \ref libusb_log_cb_mode for + * a description. + * \see libusb_log_cb, libusb_log_cb_mode + */ +void API_EXPORTED libusb_set_log_cb(libusb_context *ctx, libusb_log_cb cb, + int mode) +{ + libusb_set_log_cb_internal(ctx, cb, mode); +} + +/** \ingroup libusb_lib + * Set an option in the library. + * + * Use this function to configure a specific option within the library. + * + * Some options require one or more arguments to be provided. Consult each + * option's documentation for specific requirements. + * + * If the context ctx is NULL, the option will be added to a list of default + * options that will be applied to all subsequently created contexts. + * + * Since version 1.0.22, \ref LIBUSB_API_VERSION >= 0x01000106 + * + * \param ctx context on which to operate + * \param option which option to set + * \param ... any required arguments for the specified option + * + * \returns \ref LIBUSB_SUCCESS on success + * \returns \ref LIBUSB_ERROR_INVALID_PARAM if the option or arguments are invalid + * \returns \ref LIBUSB_ERROR_NOT_SUPPORTED if the option is valid but not supported + * on this platform + * \returns \ref LIBUSB_ERROR_NOT_FOUND if LIBUSB_OPTION_USE_USBDK is valid on this platform but UsbDk is not available + */ +int API_EXPORTEDV libusb_set_option(libusb_context *ctx, + enum libusb_option option, ...) +{ + int arg = 0, r = LIBUSB_SUCCESS; + libusb_log_cb log_cb = NULL; + va_list ap; +#if defined(ENABLE_LOGGING) && !defined(ENABLE_DEBUG_LOGGING) + int is_default_context = (NULL == ctx); +#endif + + va_start(ap, option); + + if (LIBUSB_OPTION_LOG_LEVEL == option) { + arg = va_arg(ap, int); + if (arg < LIBUSB_LOG_LEVEL_NONE || arg > LIBUSB_LOG_LEVEL_DEBUG) { + r = LIBUSB_ERROR_INVALID_PARAM; + } + } + if (LIBUSB_OPTION_LOG_CB == option) { + log_cb = (libusb_log_cb) va_arg(ap, libusb_log_cb); + } + + do { + if (LIBUSB_SUCCESS != r) { + break; + } + + if (option >= LIBUSB_OPTION_MAX) { + r = LIBUSB_ERROR_INVALID_PARAM; + break; + } + + if (NULL == ctx) { + usbi_mutex_static_lock(&default_context_lock); + default_context_options[option].is_set = 1; + if (LIBUSB_OPTION_LOG_LEVEL == option) { + default_context_options[option].arg.ival = arg; + } else if (LIBUSB_OPTION_LOG_CB == option) { + default_context_options[option].arg.log_cbval = log_cb; + libusb_set_log_cb_internal(NULL, log_cb, LIBUSB_LOG_CB_GLOBAL); + } + usbi_mutex_static_unlock(&default_context_lock); + } + + ctx = usbi_get_context(ctx); + if (NULL == ctx) + break; + + switch (option) { + case LIBUSB_OPTION_LOG_LEVEL: +#if defined(ENABLE_LOGGING) && !defined(ENABLE_DEBUG_LOGGING) + if (!ctx->debug_fixed) { + ctx->debug = (enum libusb_log_level)arg; + if (is_default_context) + usbi_atomic_store(&default_debug_level, CLAMP(arg, LIBUSB_LOG_LEVEL_NONE, LIBUSB_LOG_LEVEL_DEBUG)); + } +#endif + break; + + /* Handle all backend-specific options here */ + case LIBUSB_OPTION_USE_USBDK: + case LIBUSB_OPTION_NO_DEVICE_DISCOVERY: + if (usbi_backend.set_option) { + r = usbi_backend.set_option(ctx, option, ap); + break; + } + + r = LIBUSB_ERROR_NOT_SUPPORTED; + break; + + case LIBUSB_OPTION_LOG_CB: + libusb_set_log_cb_internal(ctx, log_cb, LIBUSB_LOG_CB_CONTEXT); + break; + + case LIBUSB_OPTION_MAX: /* unreachable */ + default: + r = LIBUSB_ERROR_INVALID_PARAM; + } + } while (0); + + va_end(ap); + + return r; +} + +#if defined(ENABLE_LOGGING) && !defined(ENABLE_DEBUG_LOGGING) +/* returns the log level as defined in the LIBUSB_DEBUG environment variable. + * if LIBUSB_DEBUG is not present or not a number, returns LIBUSB_LOG_LEVEL_NONE. + * value is clamped to ensure it is within the valid range of possibilities. + */ +static enum libusb_log_level get_env_debug_level(void) +{ + enum libusb_log_level level = LIBUSB_LOG_LEVEL_NONE; + const char *dbg = getenv("LIBUSB_DEBUG"); + if (dbg) { + long dbg_level = strtol(dbg, NULL, 10); + dbg_level = CLAMP(dbg_level, LIBUSB_LOG_LEVEL_NONE, LIBUSB_LOG_LEVEL_DEBUG); + level = (enum libusb_log_level)dbg_level; + } + return level; +} +#endif + +/** \ingroup libusb_lib + * Deprecated initialization function. Equivalent to calling libusb_init_context with no options. + * + * \see libusb_init_context + */ +int API_EXPORTED libusb_init(libusb_context **ctx) +{ + return libusb_init_context(ctx, NULL, 0); +} + +/** \ingroup libusb_lib + * Initialize libusb. This function must be called before calling any other + * libusb function. + * + * If you do not provide an output location for a context pointer, a default + * context will be created. If there was already a default context, it will + * be reused (and nothing will be initialized/reinitialized and options will + * be ignored). If num_options is 0 then options is ignored and may be NULL. + * + * Since version 1.0.27, \ref LIBUSB_API_VERSION >= 0x0100010A + * + * \param ctx Optional output location for context pointer. + * Only valid on return code 0. + * \param options Optional array of options to set on the new context. + * \param num_options Number of elements in the options array. + * \returns 0 on success, or a LIBUSB_ERROR code on failure + * \see libusb_contexts + */ +int API_EXPORTED libusb_init_context(libusb_context **ctx, const struct libusb_init_option options[], int num_options) +{ + size_t priv_size = usbi_backend.context_priv_size; + struct libusb_context *_ctx; + int r; + + usbi_mutex_static_lock(&default_context_lock); + + if (!ctx && default_context_refcnt > 0) { + usbi_dbg(usbi_default_context, "reusing default context"); + default_context_refcnt++; + usbi_mutex_static_unlock(&default_context_lock); + return 0; + } + + /* check for first init */ + usbi_mutex_static_lock(&active_contexts_lock); + if (!active_contexts_list.next) { + list_init(&active_contexts_list); + usbi_get_monotonic_time(×tamp_origin); + } + usbi_mutex_static_unlock(&active_contexts_lock); + + _ctx = calloc(1, PTR_ALIGN(sizeof(*_ctx)) + priv_size); + if (!_ctx) { + usbi_mutex_static_unlock(&default_context_lock); + return LIBUSB_ERROR_NO_MEM; + } + +#if defined(ENABLE_LOGGING) && !defined(ENABLE_DEBUG_LOGGING) + _ctx->debug = LIBUSB_LOG_LEVEL_NONE; + if (getenv("LIBUSB_DEBUG")) { + _ctx->debug = get_env_debug_level(); + _ctx->debug_fixed = 1; + } else if (default_context_options[LIBUSB_OPTION_LOG_LEVEL].is_set) { + _ctx->debug = (enum libusb_log_level)default_context_options[LIBUSB_OPTION_LOG_LEVEL].arg.ival; + } +#endif + + usbi_mutex_init(&_ctx->usb_devs_lock); + usbi_mutex_init(&_ctx->open_devs_lock); + list_init(&_ctx->usb_devs); + list_init(&_ctx->open_devs); + + /* apply default options to all new contexts */ + for (enum libusb_option option = 0 ; option < LIBUSB_OPTION_MAX ; option++) { + if (LIBUSB_OPTION_LOG_LEVEL == option || !default_context_options[option].is_set) { + continue; + } + if (LIBUSB_OPTION_LOG_CB != option) { + r = libusb_set_option(_ctx, option); + } else { + r = libusb_set_option(_ctx, option, default_context_options[option].arg.log_cbval); + } + if (LIBUSB_SUCCESS != r) + goto err_free_ctx; + } + + /* apply any options provided by the user */ + for (int i = 0 ; i < num_options ; ++i) { + switch(options[i].option) { + case LIBUSB_OPTION_LOG_CB: + r = libusb_set_option(_ctx, options[i].option, options[i].value.log_cbval); + break; + + case LIBUSB_OPTION_LOG_LEVEL: + case LIBUSB_OPTION_USE_USBDK: + case LIBUSB_OPTION_NO_DEVICE_DISCOVERY: + case LIBUSB_OPTION_MAX: + default: + r = libusb_set_option(_ctx, options[i].option, options[i].value.ival); + } + if (LIBUSB_SUCCESS != r) + goto err_free_ctx; + } + + /* default context must be initialized before calling usbi_dbg */ + if (!ctx) { + usbi_default_context = _ctx; + default_context_refcnt = 1; +#if defined(ENABLE_LOGGING) && !defined(ENABLE_DEBUG_LOGGING) + usbi_atomic_store(&default_debug_level, _ctx->debug); +#endif + usbi_dbg(usbi_default_context, "created default context"); + } + + usbi_dbg(_ctx, "libusb v%u.%u.%u.%u%s", libusb_version_internal.major, libusb_version_internal.minor, + libusb_version_internal.micro, libusb_version_internal.nano, libusb_version_internal.rc); + + r = usbi_io_init(_ctx); + if (r < 0) + goto err_free_ctx; + + usbi_mutex_static_lock(&active_contexts_lock); + list_add(&_ctx->list, &active_contexts_list); + usbi_mutex_static_unlock(&active_contexts_lock); + + if (usbi_backend.init) { + r = usbi_backend.init(_ctx); + if (r) + goto err_io_exit; + } + + /* Initialize hotplug after the initial enumeration is done. */ + usbi_hotplug_init(_ctx); + + if (ctx) { + *ctx = _ctx; + + if (!usbi_fallback_context) { +#if defined(ENABLE_LOGGING) && !defined(ENABLE_DEBUG_LOGGING) + if (usbi_atomic_load(&default_debug_level) == -1) + usbi_atomic_store(&default_debug_level, _ctx->debug); +#endif + usbi_fallback_context = _ctx; + usbi_dbg(usbi_fallback_context, "installing new context as implicit default"); + } + } + + usbi_mutex_static_unlock(&default_context_lock); + + return 0; + +err_io_exit: + usbi_mutex_static_lock(&active_contexts_lock); + list_del(&_ctx->list); + usbi_mutex_static_unlock(&active_contexts_lock); + + usbi_hotplug_exit(_ctx); + usbi_io_exit(_ctx); + +err_free_ctx: + if (!ctx) { + /* clear default context that was not fully initialized */ + usbi_default_context = NULL; + default_context_refcnt = 0; + } + + usbi_mutex_destroy(&_ctx->open_devs_lock); + usbi_mutex_destroy(&_ctx->usb_devs_lock); + + free(_ctx); + + usbi_mutex_static_unlock(&default_context_lock); + + return r; +} + +/** \ingroup libusb_lib + * Deinitialize libusb. Should be called after closing all open devices and + * before your application terminates. + * \param ctx the context to deinitialize, or NULL for the default context + */ +void API_EXPORTED libusb_exit(libusb_context *ctx) +{ + struct libusb_context *_ctx; + struct libusb_device *dev; + + usbi_mutex_static_lock(&default_context_lock); + + /* if working with default context, only actually do the deinitialization + * if we're the last user */ + if (!ctx) { + if (!usbi_default_context) { + usbi_dbg(ctx, "no default context, not initialized?"); + usbi_mutex_static_unlock(&default_context_lock); + return; + } + + if (--default_context_refcnt > 0) { + usbi_dbg(ctx, "not destroying default context"); + usbi_mutex_static_unlock(&default_context_lock); + return; + } + + usbi_dbg(ctx, "destroying default context"); + _ctx = usbi_default_context; + } else { + usbi_dbg(ctx, " "); + _ctx = ctx; + } + + usbi_mutex_static_lock(&active_contexts_lock); + list_del(&_ctx->list); + usbi_mutex_static_unlock(&active_contexts_lock); + + /* Exit hotplug before backend dependency */ + usbi_hotplug_exit(_ctx); + + if (usbi_backend.exit) + usbi_backend.exit(_ctx); + + if (!ctx) + usbi_default_context = NULL; + if (ctx == usbi_fallback_context) + usbi_fallback_context = NULL; + + usbi_mutex_static_unlock(&default_context_lock); + + /* Don't bother with locking after this point because unless there is + * an application bug, nobody will be accessing the context. */ + + usbi_io_exit(_ctx); + + for_each_device(_ctx, dev) { + usbi_warn(_ctx, "device %d.%d still referenced", + dev->bus_number, dev->device_address); + DEVICE_CTX(dev) = NULL; + } + + if (!list_empty(&_ctx->open_devs)) + usbi_warn(_ctx, "application left some devices open"); + + usbi_mutex_destroy(&_ctx->open_devs_lock); + usbi_mutex_destroy(&_ctx->usb_devs_lock); + + free(_ctx); +} + +/** \ingroup libusb_misc + * Check at runtime if the loaded library has a given capability. + * This call should be performed after \ref libusb_init_context(), to ensure the + * backend has updated its capability set. + * + * \param capability the \ref libusb_capability to check for + * \returns nonzero if the running library has the capability, 0 otherwise + */ +int API_EXPORTED libusb_has_capability(uint32_t capability) +{ + switch (capability) { + case LIBUSB_CAP_HAS_CAPABILITY: + return 1; + case LIBUSB_CAP_HAS_HOTPLUG: + return !(usbi_backend.get_device_list); + case LIBUSB_CAP_HAS_HID_ACCESS: + return (usbi_backend.caps & USBI_CAP_HAS_HID_ACCESS); + case LIBUSB_CAP_SUPPORTS_DETACH_KERNEL_DRIVER: + return (usbi_backend.caps & USBI_CAP_SUPPORTS_DETACH_KERNEL_DRIVER); + } + return 0; +} + +#ifdef ENABLE_LOGGING + +/* this is defined in libusbi.h if needed */ +#ifdef LIBUSB_PRINTF_WIN32 +/* + * Prior to VS2015, Microsoft did not provide the snprintf() function and + * provided a vsnprintf() that did not guarantee NUL-terminated output. + * Microsoft did provide a _snprintf() function, but again it did not + * guarantee NULL-terminated output. + * + * The below implementations guarantee NUL-terminated output and are + * C99 compliant. + */ + +int usbi_snprintf(char *str, size_t size, const char *format, ...) +{ + va_list args; + int ret; + + va_start(args, format); + ret = usbi_vsnprintf(str, size, format, args); + va_end(args); + + return ret; +} + +int usbi_vsnprintf(char *str, size_t size, const char *format, va_list args) +{ + int ret; + + ret = _vsnprintf(str, size, format, args); + if (ret < 0 || ret == (int)size) { + /* Output is truncated, ensure buffer is NUL-terminated and + * determine how many characters would have been written. */ + str[size - 1] = '\0'; + if (ret < 0) + ret = _vsnprintf(NULL, 0, format, args); + } + + return ret; +} +#endif /* LIBUSB_PRINTF_WIN32 */ + +static void log_str(enum libusb_log_level level, const char *str) +{ +#if defined(USE_SYSTEM_LOGGING_FACILITY) +#if defined(__ANDROID__) + int priority; + switch (level) { + case LIBUSB_LOG_LEVEL_NONE: return; /* Impossible, but keeps compiler happy */ + case LIBUSB_LOG_LEVEL_ERROR: priority = ANDROID_LOG_ERROR; break; + case LIBUSB_LOG_LEVEL_WARNING: priority = ANDROID_LOG_WARN; break; + case LIBUSB_LOG_LEVEL_INFO: priority = ANDROID_LOG_INFO; break; + case LIBUSB_LOG_LEVEL_DEBUG: priority = ANDROID_LOG_DEBUG; break; + default: priority = ANDROID_LOG_UNKNOWN; + } + __android_log_write(priority, "libusb", str); +#elif defined(_WIN32) + UNUSED(level); + OutputDebugStringA(str); +#elif defined(HAVE_SYSLOG) + int syslog_level; + switch (level) { + case LIBUSB_LOG_LEVEL_NONE: return; /* Impossible, but keeps compiler happy */ + case LIBUSB_LOG_LEVEL_ERROR: syslog_level = LOG_ERR; break; + case LIBUSB_LOG_LEVEL_WARNING: syslog_level = LOG_WARNING; break; + case LIBUSB_LOG_LEVEL_INFO: syslog_level = LOG_INFO; break; + case LIBUSB_LOG_LEVEL_DEBUG: syslog_level = LOG_DEBUG; break; + default: syslog_level = LOG_INFO; + } + syslog(syslog_level, "%s", str); +#else /* All of gcc, Clang, Xcode seem to use #warning */ +#warning System logging is not supported on this platform. Logging to stderr will be used instead. + UNUSED(level); + fputs(str, stderr); +#endif +#else + /* Global log handler */ + if (log_handler) + log_handler(NULL, level, str); + else + fputs(str, stderr); +#endif /* USE_SYSTEM_LOGGING_FACILITY */ +} + +static void log_v(struct libusb_context *ctx, enum libusb_log_level level, + const char *function, const char *format, va_list args) +{ + const char *prefix; + char buf[USBI_MAX_LOG_LEN]; + int global_debug, header_len, text_len; + static int has_debug_header_been_displayed = 0; + +#ifdef ENABLE_DEBUG_LOGGING + global_debug = 1; + UNUSED(ctx); +#else + enum libusb_log_level ctx_level; + long default_level_value; + + if (ctx) { + ctx_level = ctx->debug; + } else { + default_level_value = usbi_atomic_load(&default_debug_level); + ctx_level = default_level_value < 0 ? get_env_debug_level() : (enum libusb_log_level)default_level_value; + } + + if (ctx_level < level) + return; + + global_debug = (ctx_level == LIBUSB_LOG_LEVEL_DEBUG); +#endif + + switch (level) { + case LIBUSB_LOG_LEVEL_NONE: /* Impossible, but keeps compiler happy */ + return; + case LIBUSB_LOG_LEVEL_ERROR: + prefix = "error"; + break; + case LIBUSB_LOG_LEVEL_WARNING: + prefix = "warning"; + break; + case LIBUSB_LOG_LEVEL_INFO: + prefix = "info"; + break; + case LIBUSB_LOG_LEVEL_DEBUG: + prefix = "debug"; + break; + default: + prefix = "unknown"; + break; + } + + if (global_debug) { + struct timespec timestamp; + + if (!has_debug_header_been_displayed) { + has_debug_header_been_displayed = 1; + log_str(LIBUSB_LOG_LEVEL_DEBUG, "[timestamp] [threadID] facility level [function call] " USBI_LOG_LINE_END); + log_str(LIBUSB_LOG_LEVEL_DEBUG, "--------------------------------------------------------------------------------" USBI_LOG_LINE_END); + } + + usbi_get_monotonic_time(×tamp); + TIMESPEC_SUB(×tamp, ×tamp_origin, ×tamp); + + header_len = snprintf(buf, sizeof(buf), + "[%2ld.%06ld] [%08lx] libusb: %s [%s] ", + (long)timestamp.tv_sec, (long)(timestamp.tv_nsec / 1000L), usbi_get_tid(), prefix, function); + } else { + header_len = snprintf(buf, sizeof(buf), + "libusb: %s [%s] ", prefix, function); + } + + if (header_len < 0 || header_len >= (int)sizeof(buf)) { + /* Somehow snprintf() failed to write to the buffer, + * remove the header so something useful is output. */ + header_len = 0; + } + + text_len = vsnprintf(buf + header_len, sizeof(buf) - (size_t)header_len, + format, args); + if (text_len < 0 || text_len + header_len >= (int)sizeof(buf)) { + /* Truncated log output. On some platforms a -1 return value means + * that the output was truncated. */ + text_len = (int)sizeof(buf) - header_len; + } + if (header_len + text_len + (int)sizeof(USBI_LOG_LINE_END) >= (int)sizeof(buf)) { + /* Need to truncate the text slightly to fit on the terminator. */ + text_len -= (header_len + text_len + (int)sizeof(USBI_LOG_LINE_END)) - (int)sizeof(buf); + } + strcpy(buf + header_len + text_len, USBI_LOG_LINE_END); + + log_str(level, buf); + + /* Per-context log handler */ +#ifndef ENABLE_DEBUG_LOGGING + if (ctx && ctx->log_handler) + ctx->log_handler(ctx, level, buf); +#endif +} + +void usbi_log(struct libusb_context *ctx, enum libusb_log_level level, + const char *function, const char *format, ...) +{ + va_list args; + + va_start(args, format); + log_v(ctx, level, function, format, args); + va_end(args); +} + +#endif /* ENABLE_LOGGING */ + +/** \ingroup libusb_misc + * Returns a constant NULL-terminated string with the ASCII name of a libusb + * error or transfer status code. The caller must not free() the returned + * string. + * + * \param error_code The \ref libusb_error or libusb_transfer_status code to + * return the name of. + * \returns The error name, or the string **UNKNOWN** if the value of + * error_code is not a known error / status code. + */ +DEFAULT_VISIBILITY const char * LIBUSB_CALL libusb_error_name(int error_code) +{ + switch (error_code) { + case LIBUSB_ERROR_IO: + return "LIBUSB_ERROR_IO"; + case LIBUSB_ERROR_INVALID_PARAM: + return "LIBUSB_ERROR_INVALID_PARAM"; + case LIBUSB_ERROR_ACCESS: + return "LIBUSB_ERROR_ACCESS"; + case LIBUSB_ERROR_NO_DEVICE: + return "LIBUSB_ERROR_NO_DEVICE"; + case LIBUSB_ERROR_NOT_FOUND: + return "LIBUSB_ERROR_NOT_FOUND"; + case LIBUSB_ERROR_BUSY: + return "LIBUSB_ERROR_BUSY"; + case LIBUSB_ERROR_TIMEOUT: + return "LIBUSB_ERROR_TIMEOUT"; + case LIBUSB_ERROR_OVERFLOW: + return "LIBUSB_ERROR_OVERFLOW"; + case LIBUSB_ERROR_PIPE: + return "LIBUSB_ERROR_PIPE"; + case LIBUSB_ERROR_INTERRUPTED: + return "LIBUSB_ERROR_INTERRUPTED"; + case LIBUSB_ERROR_NO_MEM: + return "LIBUSB_ERROR_NO_MEM"; + case LIBUSB_ERROR_NOT_SUPPORTED: + return "LIBUSB_ERROR_NOT_SUPPORTED"; + case LIBUSB_ERROR_OTHER: + return "LIBUSB_ERROR_OTHER"; + + case LIBUSB_TRANSFER_ERROR: + return "LIBUSB_TRANSFER_ERROR"; + case LIBUSB_TRANSFER_TIMED_OUT: + return "LIBUSB_TRANSFER_TIMED_OUT"; + case LIBUSB_TRANSFER_CANCELLED: + return "LIBUSB_TRANSFER_CANCELLED"; + case LIBUSB_TRANSFER_STALL: + return "LIBUSB_TRANSFER_STALL"; + case LIBUSB_TRANSFER_NO_DEVICE: + return "LIBUSB_TRANSFER_NO_DEVICE"; + case LIBUSB_TRANSFER_OVERFLOW: + return "LIBUSB_TRANSFER_OVERFLOW"; + + case 0: + return "LIBUSB_SUCCESS / LIBUSB_TRANSFER_COMPLETED"; + default: + return "**UNKNOWN**"; + } +} + +/** \ingroup libusb_misc + * Returns a pointer to const struct libusb_version with the version + * (major, minor, micro, nano and rc) of the running library. + */ +DEFAULT_VISIBILITY +const struct libusb_version * LIBUSB_CALL libusb_get_version(void) +{ + return &libusb_version_internal; +} diff --git a/usbaudio/src/main/cpp/external/libusb/libusb/descriptor.c b/usbaudio/src/main/cpp/external/libusb/libusb/descriptor.c new file mode 100644 index 000000000..15130e658 --- /dev/null +++ b/usbaudio/src/main/cpp/external/libusb/libusb/descriptor.c @@ -0,0 +1,1707 @@ +/* -*- Mode: C; indent-tabs-mode:t ; c-basic-offset:8 -*- */ +/* + * USB descriptor handling functions for libusb + * Copyright © 2007 Daniel Drake + * Copyright © 2001 Johannes Erdfelt + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "libusbi.h" + +#include + +#define DESC_HEADER_LENGTH 2 + +/** @defgroup libusb_desc USB descriptors + * This page details how to examine the various standard USB descriptors + * for detected devices + */ + +static inline uint16_t ReadLittleEndian16(const uint8_t p[2]) +{ + return (uint16_t)((uint16_t)p[1] << 8 | + (uint16_t)p[0]); +} + +static inline uint32_t ReadLittleEndian32(const uint8_t p[4]) +{ + return ((uint32_t)p[3] << 24 | + (uint32_t)p[2] << 16 | + (uint32_t)p[1] << 8 | + (uint32_t)p[0]); +} + +static void clear_endpoint(struct libusb_endpoint_descriptor *endpoint) +{ + free((void *)endpoint->extra); +} + +static int parse_endpoint(struct libusb_context *ctx, + struct libusb_endpoint_descriptor *endpoint, const uint8_t *buffer, int size) +{ + const struct usbi_descriptor_header *header; + const uint8_t *begin; + void *extra; + int parsed = 0; + + if (size < DESC_HEADER_LENGTH) { + usbi_err(ctx, "short endpoint descriptor read %d/%d", + size, DESC_HEADER_LENGTH); + return LIBUSB_ERROR_IO; + } + + header = (const struct usbi_descriptor_header *)buffer; + if (header->bDescriptorType != LIBUSB_DT_ENDPOINT) { + usbi_err(ctx, "unexpected descriptor 0x%x (expected 0x%x)", + header->bDescriptorType, LIBUSB_DT_ENDPOINT); + return parsed; + } else if (header->bLength < LIBUSB_DT_ENDPOINT_SIZE) { + usbi_err(ctx, "invalid endpoint bLength (%u)", header->bLength); + return LIBUSB_ERROR_IO; + } else if (header->bLength > size) { + usbi_warn(ctx, "short endpoint descriptor read %d/%u", + size, header->bLength); + return parsed; + } + + endpoint->bLength = buffer[0]; + endpoint->bDescriptorType = buffer[1]; + endpoint->bEndpointAddress = buffer[2]; + endpoint->bmAttributes = buffer[3]; + endpoint->wMaxPacketSize = ReadLittleEndian16(&buffer[4]); + endpoint->bInterval = buffer[6]; + if (header->bLength >= LIBUSB_DT_ENDPOINT_AUDIO_SIZE) { + endpoint->bRefresh = buffer[7]; + endpoint->bSynchAddress = buffer[8]; + } + + buffer += header->bLength; + size -= header->bLength; + parsed += header->bLength; + + /* Skip over the rest of the Class Specific or Vendor Specific */ + /* descriptors */ + begin = buffer; + while (size >= DESC_HEADER_LENGTH) { + header = (const struct usbi_descriptor_header *)buffer; + if (header->bLength < DESC_HEADER_LENGTH) { + usbi_err(ctx, "invalid extra ep desc len (%u)", + header->bLength); + return LIBUSB_ERROR_IO; + } else if (header->bLength > size) { + usbi_warn(ctx, "short extra ep desc read %d/%u", + size, header->bLength); + return parsed; + } + + /* If we find another "proper" descriptor then we're done */ + if (header->bDescriptorType == LIBUSB_DT_ENDPOINT || + header->bDescriptorType == LIBUSB_DT_INTERFACE || + header->bDescriptorType == LIBUSB_DT_CONFIG || + header->bDescriptorType == LIBUSB_DT_DEVICE) + break; + + usbi_dbg(ctx, "skipping descriptor 0x%x", header->bDescriptorType); + buffer += header->bLength; + size -= header->bLength; + parsed += header->bLength; + } + + /* Copy any unknown descriptors into a storage area for drivers */ + /* to later parse */ + ptrdiff_t len = buffer - begin; + if (len <= 0) + return parsed; + + extra = malloc((size_t)len); + if (!extra) + return LIBUSB_ERROR_NO_MEM; + + memcpy(extra, begin, (size_t)len); + endpoint->extra = extra; + endpoint->extra_length = (int)len; + + return parsed; +} + +static void clear_interface(struct libusb_interface *usb_interface) +{ + int i; + + if (usb_interface->altsetting) { + for (i = 0; i < usb_interface->num_altsetting; i++) { + struct libusb_interface_descriptor *ifp = + (struct libusb_interface_descriptor *) + usb_interface->altsetting + i; + + free((void *)ifp->extra); + ifp->extra = NULL; + ifp->extra_length = 0; + if (ifp->endpoint) { + uint8_t j; + + for (j = 0; j < ifp->bNumEndpoints; j++) + clear_endpoint((struct libusb_endpoint_descriptor *) + ifp->endpoint + j); + } + free((void *)ifp->endpoint); + ifp->endpoint = NULL; + ifp->bNumEndpoints = 0; + } + } + free((void *)usb_interface->altsetting); + usb_interface->altsetting = NULL; + usb_interface->num_altsetting = 0; +} + +static int parse_interface(libusb_context *ctx, + struct libusb_interface *usb_interface, const uint8_t *buffer, int size) +{ + int r; + int parsed = 0; + int interface_number = -1; + const struct usbi_descriptor_header *header; + const struct usbi_interface_descriptor *if_desc; + struct libusb_interface_descriptor *ifp; + const uint8_t *begin; + + while (size >= LIBUSB_DT_INTERFACE_SIZE) { + struct libusb_interface_descriptor *altsetting; + + altsetting = realloc((void *)usb_interface->altsetting, + sizeof(*altsetting) * (size_t)(usb_interface->num_altsetting + 1)); + if (!altsetting) { + r = LIBUSB_ERROR_NO_MEM; + goto err; + } + usb_interface->altsetting = altsetting; + + ifp = altsetting + usb_interface->num_altsetting; + ifp->bLength = buffer[0]; + ifp->bDescriptorType = buffer[1]; + ifp->bInterfaceNumber = buffer[2]; + ifp->bAlternateSetting = buffer[3]; + ifp->bNumEndpoints = buffer[4]; + ifp->bInterfaceClass = buffer[5]; + ifp->bInterfaceSubClass = buffer[6]; + ifp->bInterfaceProtocol = buffer[7]; + ifp->iInterface = buffer[8]; + if (ifp->bDescriptorType != LIBUSB_DT_INTERFACE) { + usbi_err(ctx, "unexpected descriptor 0x%x (expected 0x%x)", + ifp->bDescriptorType, LIBUSB_DT_INTERFACE); + return parsed; + } else if (ifp->bLength < LIBUSB_DT_INTERFACE_SIZE) { + usbi_err(ctx, "invalid interface bLength (%u)", + ifp->bLength); + r = LIBUSB_ERROR_IO; + goto err; + } else if (ifp->bLength > size) { + usbi_warn(ctx, "short intf descriptor read %d/%u", + size, ifp->bLength); + return parsed; + } else if (ifp->bNumEndpoints > USB_MAXENDPOINTS) { + usbi_err(ctx, "too many endpoints (%u)", ifp->bNumEndpoints); + r = LIBUSB_ERROR_IO; + goto err; + } + + usb_interface->num_altsetting++; + ifp->extra = NULL; + ifp->extra_length = 0; + ifp->endpoint = NULL; + + if (interface_number == -1) + interface_number = ifp->bInterfaceNumber; + + /* Skip over the interface */ + buffer += ifp->bLength; + parsed += ifp->bLength; + size -= ifp->bLength; + + begin = buffer; + + /* Skip over any interface, class or vendor descriptors */ + while (size >= DESC_HEADER_LENGTH) { + header = (const struct usbi_descriptor_header *)buffer; + if (header->bLength < DESC_HEADER_LENGTH) { + usbi_err(ctx, + "invalid extra intf desc len (%u)", + header->bLength); + r = LIBUSB_ERROR_IO; + goto err; + } else if (header->bLength > size) { + usbi_warn(ctx, + "short extra intf desc read %d/%u", + size, header->bLength); + /* Keep the invariant: bNumEndpoints > 0 implies + * endpoint != NULL. The endpoint array isn't + * allocated yet on this early return. */ + ifp->bNumEndpoints = 0; + return parsed; + } + + /* If we find another "proper" descriptor then we're done */ + if (header->bDescriptorType == LIBUSB_DT_INTERFACE || + header->bDescriptorType == LIBUSB_DT_ENDPOINT || + header->bDescriptorType == LIBUSB_DT_CONFIG || + header->bDescriptorType == LIBUSB_DT_DEVICE) + break; + + buffer += header->bLength; + parsed += header->bLength; + size -= header->bLength; + } + + /* Copy any unknown descriptors into a storage area for */ + /* drivers to later parse */ + ptrdiff_t len = buffer - begin; + if (len > 0) { + void *extra = malloc((size_t)len); + + if (!extra) { + r = LIBUSB_ERROR_NO_MEM; + goto err; + } + + memcpy(extra, begin, (size_t)len); + ifp->extra = extra; + ifp->extra_length = (int)len; + } + + if (ifp->bNumEndpoints > 0) { + struct libusb_endpoint_descriptor *endpoint; + uint8_t i; + + endpoint = calloc(ifp->bNumEndpoints, sizeof(*endpoint)); + if (!endpoint) { + r = LIBUSB_ERROR_NO_MEM; + goto err; + } + + ifp->endpoint = endpoint; + for (i = 0; i < ifp->bNumEndpoints; i++) { + r = parse_endpoint(ctx, endpoint + i, buffer, size); + if (r < 0) + goto err; + if (r == 0) { + ifp->bNumEndpoints = i; + break; + } + + buffer += r; + parsed += r; + size -= r; + } + } + + /* We check to see if it's an alternate to this one */ + if_desc = (const struct usbi_interface_descriptor *)buffer; + if (size < LIBUSB_DT_INTERFACE_SIZE || + if_desc->bDescriptorType != LIBUSB_DT_INTERFACE || + if_desc->bInterfaceNumber != interface_number) + return parsed; + } + + return parsed; +err: + clear_interface(usb_interface); + return r; +} + +static void clear_configuration(struct libusb_config_descriptor *config) +{ + uint8_t i; + + if (config->interface) { + for (i = 0; i < config->bNumInterfaces; i++) + clear_interface((struct libusb_interface *) + config->interface + i); + } + free((void *)config->interface); + config->interface = NULL; + config->bNumInterfaces = 0; + free((void *)config->extra); + config->extra = NULL; + config->extra_length = 0; +} + +static int parse_configuration(struct libusb_context *ctx, + struct libusb_config_descriptor *config, const uint8_t *buffer, int size) +{ + uint8_t i; + int r; + const struct usbi_descriptor_header *header; + struct libusb_interface *usb_interface; + + if (size < LIBUSB_DT_CONFIG_SIZE) { + usbi_err(ctx, "short config descriptor read %d/%d", + size, LIBUSB_DT_CONFIG_SIZE); + return LIBUSB_ERROR_IO; + } + + config->bLength = buffer[0]; + config->bDescriptorType = buffer[1]; + config->wTotalLength = ReadLittleEndian16(&buffer[2]); + config->bNumInterfaces = buffer[4]; + config->bConfigurationValue = buffer[5]; + config->iConfiguration = buffer[6]; + config->bmAttributes = buffer[7]; + config->MaxPower = buffer[8]; + if (config->bDescriptorType != LIBUSB_DT_CONFIG) { + usbi_err(ctx, "unexpected descriptor 0x%x (expected 0x%x)", + config->bDescriptorType, LIBUSB_DT_CONFIG); + return LIBUSB_ERROR_IO; + } else if (config->bLength < LIBUSB_DT_CONFIG_SIZE) { + usbi_err(ctx, "invalid config bLength (%u)", config->bLength); + return LIBUSB_ERROR_IO; + } else if (config->bLength > size) { + usbi_err(ctx, "short config descriptor read %d/%u", + size, config->bLength); + return LIBUSB_ERROR_IO; + } else if (config->bNumInterfaces > USB_MAXINTERFACES) { + usbi_err(ctx, "too many interfaces (%u)", config->bNumInterfaces); + return LIBUSB_ERROR_IO; + } + + usb_interface = calloc(config->bNumInterfaces, sizeof(*usb_interface)); + if (!usb_interface) + return LIBUSB_ERROR_NO_MEM; + + config->interface = usb_interface; + + buffer += config->bLength; + size -= config->bLength; + + for (i = 0; i < config->bNumInterfaces; i++) { + const uint8_t *begin; + + /* Skip over the rest of the Class Specific or Vendor */ + /* Specific descriptors */ + begin = buffer; + while (size >= DESC_HEADER_LENGTH) { + header = (const struct usbi_descriptor_header *)buffer; + if (header->bLength < DESC_HEADER_LENGTH) { + usbi_err(ctx, + "invalid extra config desc len (%u)", + header->bLength); + r = LIBUSB_ERROR_IO; + goto err; + } else if (header->bLength > size) { + usbi_warn(ctx, + "short extra config desc read %d/%u", + size, header->bLength); + config->bNumInterfaces = i; + return size; + } + + /* If we find another "proper" descriptor then we're done */ + if (header->bDescriptorType == LIBUSB_DT_ENDPOINT || + header->bDescriptorType == LIBUSB_DT_INTERFACE || + header->bDescriptorType == LIBUSB_DT_CONFIG || + header->bDescriptorType == LIBUSB_DT_DEVICE) + break; + + usbi_dbg(ctx, "skipping descriptor 0x%x", header->bDescriptorType); + buffer += header->bLength; + size -= header->bLength; + } + + /* Copy any unknown descriptors into a storage area for */ + /* drivers to later parse */ + ptrdiff_t len = buffer - begin; + if (len > 0) { + uint8_t *extra = realloc((void *)config->extra, + (size_t)(config->extra_length) + (size_t)len); + + if (!extra) { + r = LIBUSB_ERROR_NO_MEM; + goto err; + } + + memcpy(extra + config->extra_length, begin, (size_t)len); + config->extra = extra; + config->extra_length += (int)len; + } + + r = parse_interface(ctx, usb_interface + i, buffer, size); + if (r < 0) + goto err; + if (r == 0) { + config->bNumInterfaces = i; + break; + } + + buffer += r; + size -= r; + } + + return size; + +err: + clear_configuration(config); + return r; +} + +static int raw_desc_to_config(struct libusb_context *ctx, + const uint8_t *buf, int size, struct libusb_config_descriptor **config) +{ + struct libusb_config_descriptor *_config = calloc(1, sizeof(*_config)); + int r; + + if (!_config) + return LIBUSB_ERROR_NO_MEM; + + r = parse_configuration(ctx, _config, buf, size); + if (r < 0) { + usbi_err(ctx, "parse_configuration failed with error %d", r); + free(_config); + return r; + } else if (r > 0) { + usbi_warn(ctx, "still %d bytes of descriptor data left", r); + } + + *config = _config; + return LIBUSB_SUCCESS; +} + +static int get_active_config_descriptor(struct libusb_device *dev, + uint8_t *buffer, size_t size) +{ + int r = usbi_backend.get_active_config_descriptor(dev, buffer, size); + + if (r < 0) + return r; + + if (r < LIBUSB_DT_CONFIG_SIZE) { + usbi_err(DEVICE_CTX(dev), "short config descriptor read %d/%d", + r, LIBUSB_DT_CONFIG_SIZE); + return LIBUSB_ERROR_IO; + } else if (r != (int)size) { + usbi_warn(DEVICE_CTX(dev), "short config descriptor read %d/%d", + r, (int)size); + } + + return r; +} + +static int get_config_descriptor(struct libusb_device *dev, uint8_t config_idx, + uint8_t *buffer, size_t size) +{ + int r = usbi_backend.get_config_descriptor(dev, config_idx, buffer, size); + + if (r < 0) + return r; + if (r < LIBUSB_DT_CONFIG_SIZE) { + usbi_err(DEVICE_CTX(dev), "short config descriptor read %d/%d", + r, LIBUSB_DT_CONFIG_SIZE); + return LIBUSB_ERROR_IO; + } else if (r != (int)size) { + usbi_warn(DEVICE_CTX(dev), "short config descriptor read %d/%d", + r, (int)size); + } + + return r; +} + +/** \ingroup libusb_desc + * Get the USB device descriptor for a given device. + * + * This is a non-blocking function; the device descriptor is cached in memory. + * + * Note since libusb-1.0.16, \ref LIBUSBX_API_VERSION >= 0x01000102, this + * function always succeeds. + * + * \param dev the device + * \param desc output location for the descriptor data + * \returns 0 on success or a LIBUSB_ERROR code on failure + */ +int API_EXPORTED libusb_get_device_descriptor(libusb_device *dev, + struct libusb_device_descriptor *desc) +{ + usbi_dbg(DEVICE_CTX(dev), " "); + static_assert(sizeof(dev->device_descriptor) == LIBUSB_DT_DEVICE_SIZE, + "struct libusb_device_descriptor is not expected size"); + *desc = dev->device_descriptor; + return 0; +} + +/** \ingroup libusb_desc + * Get the USB configuration descriptor for the currently active configuration. + * This is a non-blocking function which does not involve any requests being + * sent to the device. + * + * \param dev a device + * \param config output location for the USB configuration descriptor. Only + * valid if 0 was returned. Must be freed with libusb_free_config_descriptor() + * after use. + * \returns 0 on success + * \returns \ref LIBUSB_ERROR_NOT_FOUND if the device is in unconfigured state + * \returns another LIBUSB_ERROR code on error + * \see libusb_get_config_descriptor + */ +int API_EXPORTED libusb_get_active_config_descriptor(libusb_device *dev, + struct libusb_config_descriptor **config) +{ + union usbi_config_desc_buf _config; + uint16_t config_len; + uint8_t *buf; + int r; + + r = get_active_config_descriptor(dev, _config.buf, sizeof(_config.buf)); + if (r < 0) + return r; + + config_len = libusb_le16_to_cpu(_config.desc.wTotalLength); + buf = malloc(config_len); + if (!buf) + return LIBUSB_ERROR_NO_MEM; + + r = get_active_config_descriptor(dev, buf, config_len); + if (r >= 0) + r = raw_desc_to_config(DEVICE_CTX(dev), buf, r, config); + + free(buf); + return r; +} + +/** \ingroup libusb_desc + * Get a USB configuration descriptor based on its index. + * This is a non-blocking function which does not involve any requests being + * sent to the device. + * + * \param dev a device + * \param config_index the index of the configuration you wish to retrieve + * \param config output location for the USB configuration descriptor. Only + * valid if 0 was returned. Must be freed with libusb_free_config_descriptor() + * after use. + * \returns 0 on success + * \returns \ref LIBUSB_ERROR_NOT_FOUND if the configuration does not exist + * \returns another LIBUSB_ERROR code on error + * \see libusb_get_active_config_descriptor() + * \see libusb_get_config_descriptor_by_value() + */ +int API_EXPORTED libusb_get_config_descriptor(libusb_device *dev, + uint8_t config_index, struct libusb_config_descriptor **config) +{ + union usbi_config_desc_buf _config; + uint16_t config_len; + uint8_t *buf; + int r; + + usbi_dbg(DEVICE_CTX(dev), "index %u", config_index); + if (config_index >= dev->device_descriptor.bNumConfigurations) + return LIBUSB_ERROR_NOT_FOUND; + + r = get_config_descriptor(dev, config_index, _config.buf, sizeof(_config.buf)); + if (r < 0) + return r; + + config_len = libusb_le16_to_cpu(_config.desc.wTotalLength); + buf = malloc(config_len); + if (!buf) + return LIBUSB_ERROR_NO_MEM; + + r = get_config_descriptor(dev, config_index, buf, config_len); + if (r >= 0) + r = raw_desc_to_config(DEVICE_CTX(dev), buf, r, config); + + free(buf); + return r; +} + +/** \ingroup libusb_desc + * Get a USB configuration descriptor with a specific bConfigurationValue. + * This is a non-blocking function which does not involve any requests being + * sent to the device. + * + * \param dev a device + * \param bConfigurationValue the bConfigurationValue of the configuration you + * wish to retrieve + * \param config output location for the USB configuration descriptor. Only + * valid if 0 was returned. Must be freed with libusb_free_config_descriptor() + * after use. + * \returns 0 on success + * \returns \ref LIBUSB_ERROR_NOT_FOUND if the configuration does not exist + * \returns another LIBUSB_ERROR code on error + * \see libusb_get_active_config_descriptor() + * \see libusb_get_config_descriptor() + */ +int API_EXPORTED libusb_get_config_descriptor_by_value(libusb_device *dev, + uint8_t bConfigurationValue, struct libusb_config_descriptor **config) +{ + uint8_t idx; + int r; + + if (usbi_backend.get_config_descriptor_by_value) { + void *buf; + + r = usbi_backend.get_config_descriptor_by_value(dev, + bConfigurationValue, &buf); + if (r < 0) + return r; + + return raw_desc_to_config(DEVICE_CTX(dev), buf, r, config); + } + + usbi_dbg(DEVICE_CTX(dev), "value %u", bConfigurationValue); + for (idx = 0; idx < dev->device_descriptor.bNumConfigurations; idx++) { + union usbi_config_desc_buf _config; + + r = get_config_descriptor(dev, idx, _config.buf, sizeof(_config.buf)); + if (r < 0) + return r; + + if (_config.desc.bConfigurationValue == bConfigurationValue) + return libusb_get_config_descriptor(dev, idx, config); + } + + return LIBUSB_ERROR_NOT_FOUND; +} + +/** \ingroup libusb_desc + * Free a configuration descriptor obtained from + * libusb_get_active_config_descriptor() or libusb_get_config_descriptor(). + * It is safe to call this function with a NULL config parameter, in which + * case the function simply returns. + * + * \param config the configuration descriptor to free + */ +void API_EXPORTED libusb_free_config_descriptor( + struct libusb_config_descriptor *config) +{ + if (!config) + return; + + clear_configuration(config); + free(config); +} + +/** \ingroup libusb_desc + * Get an endpoints superspeed endpoint companion descriptor (if any) + * + * \param ctx the context to operate on, or NULL for the default context + * \param endpoint endpoint descriptor from which to get the superspeed + * endpoint companion descriptor + * \param ep_comp output location for the superspeed endpoint companion + * descriptor. Only valid if 0 was returned. Must be freed with + * libusb_free_ss_endpoint_companion_descriptor() after use. + * \returns 0 on success + * \returns \ref LIBUSB_ERROR_NOT_FOUND if the configuration does not exist + * \returns another LIBUSB_ERROR code on error + */ +int API_EXPORTED libusb_get_ss_endpoint_companion_descriptor( + libusb_context *ctx, + const struct libusb_endpoint_descriptor *endpoint, + struct libusb_ss_endpoint_companion_descriptor **ep_comp) +{ + const struct usbi_descriptor_header *header; + const uint8_t *buffer = endpoint->extra; + int size = endpoint->extra_length; + + *ep_comp = NULL; + + while (size >= DESC_HEADER_LENGTH) { + header = (const struct usbi_descriptor_header *)buffer; + if (header->bDescriptorType != LIBUSB_DT_SS_ENDPOINT_COMPANION) { + if (header->bLength < DESC_HEADER_LENGTH) { + usbi_err(ctx, "invalid descriptor length %u", + header->bLength); + return LIBUSB_ERROR_IO; + } + buffer += header->bLength; + size -= header->bLength; + continue; + } else if (header->bLength < LIBUSB_DT_SS_ENDPOINT_COMPANION_SIZE) { + usbi_err(ctx, "invalid ss-ep-comp-desc length %u", + header->bLength); + return LIBUSB_ERROR_IO; + } else if (header->bLength > size) { + usbi_err(ctx, "short ss-ep-comp-desc read %d/%u", + size, header->bLength); + return LIBUSB_ERROR_IO; + } + + *ep_comp = malloc(sizeof(**ep_comp)); + if (!*ep_comp) + return LIBUSB_ERROR_NO_MEM; + (*ep_comp)->bLength = buffer[0]; + (*ep_comp)->bDescriptorType = buffer[1]; + (*ep_comp)->bMaxBurst = buffer[2]; + (*ep_comp)->bmAttributes = buffer[3]; + (*ep_comp)->wBytesPerInterval = ReadLittleEndian16(&buffer[4]); + return LIBUSB_SUCCESS; + } + return LIBUSB_ERROR_NOT_FOUND; +} + +/** \ingroup libusb_desc + * Free a superspeed endpoint companion descriptor obtained from + * libusb_get_ss_endpoint_companion_descriptor(). + * It is safe to call this function with a NULL ep_comp parameter, in which + * case the function simply returns. + * + * \param ep_comp the superspeed endpoint companion descriptor to free + */ +void API_EXPORTED libusb_free_ss_endpoint_companion_descriptor( + struct libusb_ss_endpoint_companion_descriptor *ep_comp) +{ + free(ep_comp); +} + +static int parse_bos(struct libusb_context *ctx, + struct libusb_bos_descriptor **bos, + const uint8_t *buffer, int size) +{ + struct libusb_bos_descriptor *_bos; + const struct usbi_bos_descriptor *bos_desc; + const struct usbi_descriptor_header *header; + uint8_t i; + + if (size < LIBUSB_DT_BOS_SIZE) { + usbi_err(ctx, "short bos descriptor read %d/%d", + size, LIBUSB_DT_BOS_SIZE); + return LIBUSB_ERROR_IO; + } + + bos_desc = (const struct usbi_bos_descriptor *)buffer; + if (bos_desc->bDescriptorType != LIBUSB_DT_BOS) { + usbi_err(ctx, "unexpected descriptor 0x%x (expected 0x%x)", + bos_desc->bDescriptorType, LIBUSB_DT_BOS); + return LIBUSB_ERROR_IO; + } else if (bos_desc->bLength < LIBUSB_DT_BOS_SIZE) { + usbi_err(ctx, "invalid bos bLength (%u)", bos_desc->bLength); + return LIBUSB_ERROR_IO; + } else if (bos_desc->bLength > size) { + usbi_err(ctx, "short bos descriptor read %d/%u", + size, bos_desc->bLength); + return LIBUSB_ERROR_IO; + } + + _bos = calloc(1, sizeof(*_bos) + (bos_desc->bNumDeviceCaps * sizeof(void *))); + if (!_bos) + return LIBUSB_ERROR_NO_MEM; + + _bos->bLength = buffer[0]; + _bos->bDescriptorType = buffer[1]; + _bos->wTotalLength = ReadLittleEndian16(&buffer[2]); + _bos->bNumDeviceCaps = buffer[4]; + buffer += _bos->bLength; + size -= _bos->bLength; + + /* Get the device capability descriptors */ + for (i = 0; i < _bos->bNumDeviceCaps; i++) { + if (size < LIBUSB_DT_DEVICE_CAPABILITY_SIZE) { + usbi_warn(ctx, "short dev-cap descriptor read %d/%d", + size, LIBUSB_DT_DEVICE_CAPABILITY_SIZE); + break; + } + header = (const struct usbi_descriptor_header *)buffer; + if (header->bDescriptorType != LIBUSB_DT_DEVICE_CAPABILITY) { + usbi_warn(ctx, "unexpected descriptor 0x%x (expected 0x%x)", + header->bDescriptorType, LIBUSB_DT_DEVICE_CAPABILITY); + break; + } else if (header->bLength < LIBUSB_DT_DEVICE_CAPABILITY_SIZE) { + usbi_err(ctx, "invalid dev-cap bLength (%u)", + header->bLength); + libusb_free_bos_descriptor(_bos); + return LIBUSB_ERROR_IO; + } else if (header->bLength > size) { + usbi_warn(ctx, "short dev-cap descriptor read %d/%u", + size, header->bLength); + break; + } + + _bos->dev_capability[i] = malloc(header->bLength); + if (!_bos->dev_capability[i]) { + libusb_free_bos_descriptor(_bos); + return LIBUSB_ERROR_NO_MEM; + } + memcpy(_bos->dev_capability[i], buffer, header->bLength); + buffer += header->bLength; + size -= header->bLength; + } + _bos->bNumDeviceCaps = i; + *bos = _bos; + + return LIBUSB_SUCCESS; +} + +/** \ingroup libusb_desc + * Get a Binary Object Store (BOS) descriptor + * This is a BLOCKING function, which will send requests to the device. + * + * \param dev_handle the handle of an open libusb device + * \param bos output location for the BOS descriptor. Only valid if 0 was returned. + * Must be freed with \ref libusb_free_bos_descriptor() after use. + * \returns 0 on success + * \returns \ref LIBUSB_ERROR_NOT_FOUND if the device doesn't have a BOS descriptor + * \returns another LIBUSB_ERROR code on error + */ +int API_EXPORTED libusb_get_bos_descriptor(libusb_device_handle *dev_handle, + struct libusb_bos_descriptor **bos) +{ + union usbi_bos_desc_buf _bos; + uint16_t bos_len; + uint8_t *bos_data; + int r; + struct libusb_context *ctx = HANDLE_CTX(dev_handle); + + /* Read the BOS. This generates 2 requests on the bus, + * one for the header, and one for the full BOS */ + r = libusb_get_descriptor(dev_handle, LIBUSB_DT_BOS, 0, _bos.buf, sizeof(_bos.buf)); + if (r < 0) { + if (r != LIBUSB_ERROR_PIPE) + usbi_err(ctx, "failed to read BOS (%d)", r); + return r; + } + if (r < LIBUSB_DT_BOS_SIZE) { + usbi_err(ctx, "short BOS read %d/%d", + r, LIBUSB_DT_BOS_SIZE); + return LIBUSB_ERROR_IO; + } + + bos_len = libusb_le16_to_cpu(_bos.desc.wTotalLength); + usbi_dbg(ctx, "found BOS descriptor: size %u bytes, %u capabilities", + bos_len, _bos.desc.bNumDeviceCaps); + bos_data = calloc(1, bos_len); + if (!bos_data) + return LIBUSB_ERROR_NO_MEM; + + r = libusb_get_descriptor(dev_handle, LIBUSB_DT_BOS, 0, bos_data, bos_len); + if (r >= 0) { + if (r != (int)bos_len) + usbi_warn(ctx, "short BOS read %d/%u", r, bos_len); + r = parse_bos(HANDLE_CTX(dev_handle), bos, bos_data, r); + } else { + usbi_err(ctx, "failed to read BOS (%d)", r); + } + + free(bos_data); + return r; +} + +/** \ingroup libusb_desc + * Free a BOS descriptor obtained from libusb_get_bos_descriptor(). + * It is safe to call this function with a NULL bos parameter, in which + * case the function simply returns. + * + * \param bos the BOS descriptor to free + */ +void API_EXPORTED libusb_free_bos_descriptor(struct libusb_bos_descriptor *bos) +{ + uint8_t i; + + if (!bos) + return; + + for (i = 0; i < bos->bNumDeviceCaps; i++) + free(bos->dev_capability[i]); + free(bos); +} + +/** \ingroup libusb_desc + * Get an USB 2.0 Extension descriptor + * + * \param ctx the context to operate on, or NULL for the default context + * \param dev_cap Device Capability descriptor with a bDevCapabilityType of + * \ref libusb_bos_type::LIBUSB_BT_USB_2_0_EXTENSION + * LIBUSB_BT_USB_2_0_EXTENSION + * \param usb_2_0_extension output location for the USB 2.0 Extension + * descriptor. Only valid if 0 was returned. Must be freed with + * libusb_free_usb_2_0_extension_descriptor() after use. + * \returns 0 on success + * \returns a LIBUSB_ERROR code on error + */ +int API_EXPORTED libusb_get_usb_2_0_extension_descriptor( + libusb_context *ctx, + struct libusb_bos_dev_capability_descriptor *dev_cap, + struct libusb_usb_2_0_extension_descriptor **usb_2_0_extension) +{ + struct libusb_usb_2_0_extension_descriptor *_usb_2_0_extension; + + if (dev_cap->bDevCapabilityType != LIBUSB_BT_USB_2_0_EXTENSION) { + usbi_err(ctx, "unexpected bDevCapabilityType 0x%x (expected 0x%x)", + dev_cap->bDevCapabilityType, + LIBUSB_BT_USB_2_0_EXTENSION); + return LIBUSB_ERROR_INVALID_PARAM; + } else if (dev_cap->bLength < LIBUSB_BT_USB_2_0_EXTENSION_SIZE) { + usbi_err(ctx, "short dev-cap descriptor read %u/%d", + dev_cap->bLength, LIBUSB_BT_USB_2_0_EXTENSION_SIZE); + return LIBUSB_ERROR_IO; + } + + _usb_2_0_extension = malloc(sizeof(*_usb_2_0_extension)); + if (!_usb_2_0_extension) + return LIBUSB_ERROR_NO_MEM; + + _usb_2_0_extension->bLength = dev_cap->bLength; + _usb_2_0_extension->bDescriptorType = dev_cap->bDescriptorType; + _usb_2_0_extension->bDevCapabilityType = dev_cap->bDevCapabilityType; + _usb_2_0_extension->bmAttributes = ReadLittleEndian32(dev_cap->dev_capability_data); + + *usb_2_0_extension = _usb_2_0_extension; + return LIBUSB_SUCCESS; +} + +/** \ingroup libusb_desc + * Free a USB 2.0 Extension descriptor obtained from + * libusb_get_usb_2_0_extension_descriptor(). + * It is safe to call this function with a NULL usb_2_0_extension parameter, + * in which case the function simply returns. + * + * \param usb_2_0_extension the USB 2.0 Extension descriptor to free + */ +void API_EXPORTED libusb_free_usb_2_0_extension_descriptor( + struct libusb_usb_2_0_extension_descriptor *usb_2_0_extension) +{ + free(usb_2_0_extension); +} + +/** \ingroup libusb_desc + * Get a SuperSpeed USB Device Capability descriptor + * + * \param ctx the context to operate on, or NULL for the default context + * \param dev_cap Device Capability descriptor with a bDevCapabilityType of + * \ref libusb_bos_type::LIBUSB_BT_SS_USB_DEVICE_CAPABILITY + * LIBUSB_BT_SS_USB_DEVICE_CAPABILITY + * \param ss_usb_device_cap output location for the SuperSpeed USB Device + * Capability descriptor. Only valid if 0 was returned. Must be freed with + * libusb_free_ss_usb_device_capability_descriptor() after use. + * \returns 0 on success + * \returns a LIBUSB_ERROR code on error + */ +int API_EXPORTED libusb_get_ss_usb_device_capability_descriptor( + libusb_context *ctx, + struct libusb_bos_dev_capability_descriptor *dev_cap, + struct libusb_ss_usb_device_capability_descriptor **ss_usb_device_cap) +{ + struct libusb_ss_usb_device_capability_descriptor *_ss_usb_device_cap; + + if (dev_cap->bDevCapabilityType != LIBUSB_BT_SS_USB_DEVICE_CAPABILITY) { + usbi_err(ctx, "unexpected bDevCapabilityType 0x%x (expected 0x%x)", + dev_cap->bDevCapabilityType, + LIBUSB_BT_SS_USB_DEVICE_CAPABILITY); + return LIBUSB_ERROR_INVALID_PARAM; + } else if (dev_cap->bLength < LIBUSB_BT_SS_USB_DEVICE_CAPABILITY_SIZE) { + usbi_err(ctx, "short dev-cap descriptor read %u/%d", + dev_cap->bLength, LIBUSB_BT_SS_USB_DEVICE_CAPABILITY_SIZE); + return LIBUSB_ERROR_IO; + } + + _ss_usb_device_cap = malloc(sizeof(*_ss_usb_device_cap)); + if (!_ss_usb_device_cap) + return LIBUSB_ERROR_NO_MEM; + + _ss_usb_device_cap->bLength = dev_cap->bLength; + _ss_usb_device_cap->bDescriptorType = dev_cap->bDescriptorType; + _ss_usb_device_cap->bDevCapabilityType = dev_cap->bDevCapabilityType; + _ss_usb_device_cap->bmAttributes = dev_cap->dev_capability_data[0]; + _ss_usb_device_cap->wSpeedSupported = ReadLittleEndian16(&dev_cap->dev_capability_data[1]); + _ss_usb_device_cap->bFunctionalitySupport = dev_cap->dev_capability_data[3]; + _ss_usb_device_cap->bU1DevExitLat = dev_cap->dev_capability_data[4]; + _ss_usb_device_cap->bU2DevExitLat = ReadLittleEndian16(&dev_cap->dev_capability_data[5]); + + *ss_usb_device_cap = _ss_usb_device_cap; + return LIBUSB_SUCCESS; +} + +/// @cond DEV +/** \internal \ingroup libusb_desc + * We use this private struct only to parse a SuperSpeedPlus device capability + * descriptor according to section 9.6.2.5 of the USB 3.1 specification. + * We don't expose it. + */ +struct internal_ssplus_capability_descriptor { + /** The length of the descriptor. Must be equal to LIBUSB_BT_SSPLUS_USB_DEVICE_CAPABILITY_SIZE */ + uint8_t bLength; + + /** The type of the descriptor */ + uint8_t bDescriptorType; + + /** Must be equal to LIBUSB_BT_SUPERSPEED_PLUS_CAPABILITY */ + uint8_t bDevCapabilityType; + + /** Unused */ + uint8_t bReserved; + + /** Contains the number of SublinkSpeedIDs */ + uint32_t bmAttributes; + + /** Contains the ssid, minRxLaneCount, and minTxLaneCount */ + uint16_t wFunctionalitySupport; + + /** Unused */ + uint16_t wReserved; +}; +/// @endcond + +/** \ingroup libusb_desc + * Get a SuperSpeedPlus USB Device Capability descriptor + * + * Since version 1.0.28, \ref LIBUSB_API_VERSION >= 0x0100010B + * + * \param ctx the context to operate on, or NULL for the default context + * \param dev_cap Device Capability descriptor with a bDevCapabilityType of + * \ref libusb_bos_type::LIBUSB_BT_SUPERSPEED_PLUS_CAPABILITY + * LIBUSB_BT_SUPERSPEED_PLUS_CAPABILITY + * \param ssplus_usb_device_cap output location for the SuperSpeedPlus USB Device + * Capability descriptor. Only valid if 0 was returned. Must be freed with + * libusb_free_ssplus_usb_device_capability_descriptor() after use. + * \returns 0 on success + * \returns a LIBUSB_ERROR code on error + */ +int API_EXPORTED libusb_get_ssplus_usb_device_capability_descriptor( + libusb_context *ctx, + struct libusb_bos_dev_capability_descriptor *dev_cap, + struct libusb_ssplus_usb_device_capability_descriptor **ssplus_usb_device_cap) +{ + struct libusb_ssplus_usb_device_capability_descriptor *_ssplus_cap; + + /* Use a private struct to reuse our descriptor parsing system. */ + struct internal_ssplus_capability_descriptor parsedDescriptor; + + /* Some size/type checks to make sure everything is in order */ + if (dev_cap->bDevCapabilityType != LIBUSB_BT_SUPERSPEED_PLUS_CAPABILITY) { + usbi_err(ctx, "unexpected bDevCapabilityType 0x%x (expected 0x%x)", + dev_cap->bDevCapabilityType, + LIBUSB_BT_SUPERSPEED_PLUS_CAPABILITY); + return LIBUSB_ERROR_INVALID_PARAM; + } else if (dev_cap->bLength < LIBUSB_BT_SSPLUS_USB_DEVICE_CAPABILITY_SIZE) { + usbi_err(ctx, "short dev-cap descriptor read %u/%d", + dev_cap->bLength, LIBUSB_BT_SSPLUS_USB_DEVICE_CAPABILITY_SIZE); + return LIBUSB_ERROR_IO; + } + + const uint8_t* dev_capability_data = dev_cap->dev_capability_data; + parsedDescriptor.bLength = dev_cap->bLength; + parsedDescriptor.bDescriptorType = dev_cap->bDescriptorType; + parsedDescriptor.bDevCapabilityType = dev_cap->bDevCapabilityType; + parsedDescriptor.bReserved = dev_capability_data[0]; + parsedDescriptor.bmAttributes = ReadLittleEndian32(&dev_capability_data[1]); + parsedDescriptor.wFunctionalitySupport = ReadLittleEndian16(&dev_capability_data[5]); + parsedDescriptor.wReserved = ReadLittleEndian16(&dev_capability_data[7]); + + uint8_t numSublikSpeedAttributes = (parsedDescriptor.bmAttributes & 0xF) + 1; + _ssplus_cap = malloc(sizeof(struct libusb_ssplus_usb_device_capability_descriptor) + (numSublikSpeedAttributes * sizeof(struct libusb_ssplus_sublink_attribute))); + if (!_ssplus_cap) + return LIBUSB_ERROR_NO_MEM; + + /* Parse bmAttributes */ + _ssplus_cap->numSublinkSpeedAttributes = numSublikSpeedAttributes; + _ssplus_cap->numSublinkSpeedIDs = ((parsedDescriptor.bmAttributes & 0xF0) >> 4) + 1; + + /* Parse wFunctionalitySupport */ + _ssplus_cap->ssid = parsedDescriptor.wFunctionalitySupport & 0xF; + _ssplus_cap->minRxLaneCount = (parsedDescriptor.wFunctionalitySupport & 0x0F00) >> 8; + _ssplus_cap->minTxLaneCount = (parsedDescriptor.wFunctionalitySupport & 0xF000) >> 12; + + /* Check that we have enough to read all the sublink attributes */ + if (dev_cap->bLength < LIBUSB_BT_SSPLUS_USB_DEVICE_CAPABILITY_SIZE + (_ssplus_cap->numSublinkSpeedAttributes * sizeof(uint32_t))) { + free(_ssplus_cap); + usbi_err(ctx, "short ssplus capability descriptor, unable to read sublinks: Not enough data"); + return LIBUSB_ERROR_IO; + } + + /* Read the attributes */ + uint8_t* base = ((uint8_t*)dev_cap) + LIBUSB_BT_SSPLUS_USB_DEVICE_CAPABILITY_SIZE; + for(uint8_t i = 0 ; i < _ssplus_cap->numSublinkSpeedAttributes ; i++) { + uint32_t attr = ReadLittleEndian32(base + (i * sizeof(uint32_t))); + _ssplus_cap->sublinkSpeedAttributes[i].ssid = attr & 0x0f; + _ssplus_cap->sublinkSpeedAttributes[i].mantissa = attr >> 16; + _ssplus_cap->sublinkSpeedAttributes[i].exponent = (attr >> 4) & 0x3 ; + _ssplus_cap->sublinkSpeedAttributes[i].type = attr & 0x40 ? LIBUSB_SSPLUS_ATTR_TYPE_ASYM : LIBUSB_SSPLUS_ATTR_TYPE_SYM; + _ssplus_cap->sublinkSpeedAttributes[i].direction = attr & 0x80 ? LIBUSB_SSPLUS_ATTR_DIR_TX : LIBUSB_SSPLUS_ATTR_DIR_RX; + _ssplus_cap->sublinkSpeedAttributes[i].protocol = attr & 0x4000 ? LIBUSB_SSPLUS_ATTR_PROT_SSPLUS: LIBUSB_SSPLUS_ATTR_PROT_SS; + } + + *ssplus_usb_device_cap = _ssplus_cap; + return LIBUSB_SUCCESS; +} + +/** \ingroup libusb_desc + * Free a SuperSpeedPlus USB Device Capability descriptor obtained from + * libusb_get_ssplus_usb_device_capability_descriptor(). + * It is safe to call this function with a NULL ssplus_usb_device_cap + * parameter, in which case the function simply returns. + * + * Since version 1.0.28, \ref LIBUSB_API_VERSION >= 0x0100010B + * + * \param ssplus_usb_device_cap the SuperSpeedPlus USB Device Capability descriptor + * to free + */ +void API_EXPORTED libusb_free_ssplus_usb_device_capability_descriptor( + struct libusb_ssplus_usb_device_capability_descriptor *ssplus_usb_device_cap) +{ + free(ssplus_usb_device_cap); +} + + + +/** \ingroup libusb_desc + * Free a SuperSpeed USB Device Capability descriptor obtained from + * libusb_get_ss_usb_device_capability_descriptor(). + * It is safe to call this function with a NULL ss_usb_device_cap + * parameter, in which case the function simply returns. + * + * \param ss_usb_device_cap the SuperSpeed USB Device Capability descriptor + * to free + */ +void API_EXPORTED libusb_free_ss_usb_device_capability_descriptor( + struct libusb_ss_usb_device_capability_descriptor *ss_usb_device_cap) +{ + free(ss_usb_device_cap); +} + +/** \ingroup libusb_desc + * Get a Container ID descriptor + * + * \param ctx the context to operate on, or NULL for the default context + * \param dev_cap Device Capability descriptor with a bDevCapabilityType of + * \ref libusb_bos_type::LIBUSB_BT_CONTAINER_ID + * LIBUSB_BT_CONTAINER_ID + * \param container_id output location for the Container ID descriptor. + * Only valid if 0 was returned. Must be freed with + * libusb_free_container_id_descriptor() after use. + * \returns 0 on success + * \returns a LIBUSB_ERROR code on error + */ +int API_EXPORTED libusb_get_container_id_descriptor(libusb_context *ctx, + struct libusb_bos_dev_capability_descriptor *dev_cap, + struct libusb_container_id_descriptor **container_id) +{ + struct libusb_container_id_descriptor *_container_id; + + if (dev_cap->bDevCapabilityType != LIBUSB_BT_CONTAINER_ID) { + usbi_err(ctx, "unexpected bDevCapabilityType 0x%x (expected 0x%x)", + dev_cap->bDevCapabilityType, + LIBUSB_BT_CONTAINER_ID); + return LIBUSB_ERROR_INVALID_PARAM; + } else if (dev_cap->bLength < LIBUSB_BT_CONTAINER_ID_SIZE) { + usbi_err(ctx, "short dev-cap descriptor read %u/%d", + dev_cap->bLength, LIBUSB_BT_CONTAINER_ID_SIZE); + return LIBUSB_ERROR_IO; + } + + _container_id = malloc(sizeof(*_container_id)); + if (!_container_id) + return LIBUSB_ERROR_NO_MEM; + + _container_id->bLength = dev_cap->bLength; + _container_id->bDescriptorType = dev_cap->bDescriptorType; + _container_id->bDevCapabilityType = dev_cap->bDevCapabilityType; + _container_id->bReserved = dev_cap->dev_capability_data[0]; + memcpy(_container_id->ContainerID, &dev_cap->dev_capability_data[1], 16); + + *container_id = _container_id; + return LIBUSB_SUCCESS; +} + +/** \ingroup libusb_desc + * Free a Container ID descriptor obtained from + * libusb_get_container_id_descriptor(). + * It is safe to call this function with a NULL container_id parameter, + * in which case the function simply returns. + * + * \param container_id the Container ID descriptor to free + */ +void API_EXPORTED libusb_free_container_id_descriptor( + struct libusb_container_id_descriptor *container_id) +{ + free(container_id); +} + +/** \ingroup libusb_desc + * Get a platform descriptor + * + * Since version 1.0.27, \ref LIBUSB_API_VERSION >= 0x0100010A + * + * \param ctx the context to operate on, or NULL for the default context + * \param dev_cap Device Capability descriptor with a bDevCapabilityType of + * \ref libusb_bos_type::LIBUSB_BT_PLATFORM_DESCRIPTOR + * LIBUSB_BT_PLATFORM_DESCRIPTOR + * \param platform_descriptor output location for the Platform descriptor. + * Only valid if 0 was returned. Must be freed with + * libusb_free_platform_descriptor() after use. + * \returns 0 on success + * \returns a LIBUSB_ERROR code on error + */ +int API_EXPORTED libusb_get_platform_descriptor(libusb_context *ctx, + struct libusb_bos_dev_capability_descriptor *dev_cap, + struct libusb_platform_descriptor **platform_descriptor) +{ + struct libusb_platform_descriptor *_platform_descriptor; + + if (dev_cap->bDevCapabilityType != LIBUSB_BT_PLATFORM_DESCRIPTOR) { + usbi_err(ctx, "unexpected bDevCapabilityType 0x%x (expected 0x%x)", + dev_cap->bDevCapabilityType, + LIBUSB_BT_PLATFORM_DESCRIPTOR); + return LIBUSB_ERROR_INVALID_PARAM; + } else if (dev_cap->bLength < LIBUSB_BT_PLATFORM_DESCRIPTOR_MIN_SIZE) { + usbi_err(ctx, "short dev-cap descriptor read %u/%d", + dev_cap->bLength, LIBUSB_BT_PLATFORM_DESCRIPTOR_MIN_SIZE); + return LIBUSB_ERROR_IO; + } + + _platform_descriptor = malloc(dev_cap->bLength); + if (!_platform_descriptor) + return LIBUSB_ERROR_NO_MEM; + + _platform_descriptor->bLength = dev_cap->bLength; + _platform_descriptor->bDescriptorType = dev_cap->bDescriptorType; + _platform_descriptor->bDevCapabilityType = dev_cap->bDevCapabilityType; + _platform_descriptor->bReserved = dev_cap->dev_capability_data[0]; + memcpy(_platform_descriptor->PlatformCapabilityUUID, &(dev_cap->dev_capability_data[1]), 16); + + /* Capability data is located after reserved byte and 16 byte UUID */ + uint8_t* capability_data = dev_cap->dev_capability_data + 1 + 16; + + /* Capability data length is total descriptor length minus initial fields */ + size_t capability_data_length = dev_cap->bLength - (3 + 1 + 16); + + memcpy(_platform_descriptor->CapabilityData, capability_data, capability_data_length); + + *platform_descriptor = _platform_descriptor; + return LIBUSB_SUCCESS; +} + +/** \ingroup libusb_desc + * Free a platform descriptor obtained from + * libusb_get_platform_descriptor(). + * It is safe to call this function with a NULL platform_descriptor parameter, + * in which case the function simply returns. + * + * \param platform_descriptor the Platform descriptor to free + */ +void API_EXPORTED libusb_free_platform_descriptor( + struct libusb_platform_descriptor *platform_descriptor) +{ + free(platform_descriptor); +} + +/** \ingroup libusb_desc + * Retrieve a string descriptor in C style ASCII. + * + * Wrapper around libusb_get_string_descriptor(). Uses the first language + * supported by the device. + * + * \param dev_handle a device handle + * \param desc_index the index of the descriptor to retrieve + * \param data output buffer for ASCII string descriptor + * \param length size of data buffer + * \returns number of bytes returned in data, or LIBUSB_ERROR code on failure + */ +int API_EXPORTED libusb_get_string_descriptor_ascii(libusb_device_handle *dev_handle, + uint8_t desc_index, unsigned char *data, int length) +{ + union usbi_string_desc_buf str; + int r; + uint16_t langid, wdata; + + /* Asking for the zero'th index is special - it returns a string + * descriptor that contains all the language IDs supported by the + * device. Typically there aren't many - often only one. Language + * IDs are 16 bit numbers, and they start at the third byte in the + * descriptor. There's also no point in trying to read descriptor 0 + * with this function. See USB 2.0 specification section 9.6.7 for + * more information. + */ + + if (desc_index == 0) + return LIBUSB_ERROR_INVALID_PARAM; + + r = libusb_get_string_descriptor(dev_handle, 0, 0, str.buf, 4); + if (r < 0) + return r; + else if (r != 4 || str.desc.bLength < 4 || str.desc.bDescriptorType != LIBUSB_DT_STRING) { + usbi_warn(HANDLE_CTX(dev_handle), "invalid language ID string descriptor"); + return LIBUSB_ERROR_IO; + } else if (str.desc.bLength & 1) + usbi_warn(HANDLE_CTX(dev_handle), "suspicious bLength %u for language ID string descriptor", str.desc.bLength); + + langid = libusb_le16_to_cpu(str.desc.wData[0]); + r = libusb_get_string_descriptor(dev_handle, desc_index, langid, str.buf, sizeof(str.buf)); + if (r < 0) + return r; + else if (r < DESC_HEADER_LENGTH || str.desc.bLength > r || str.desc.bDescriptorType != LIBUSB_DT_STRING) + return LIBUSB_ERROR_IO; + else if ((str.desc.bLength & 1) || str.desc.bLength != r) + usbi_warn(HANDLE_CTX(dev_handle), "suspicious bLength %u for string descriptor (read %d)", str.desc.bLength, r); + + /* Stop one byte before the end to leave room for null termination. */ + int dest_max = length - 1; + + /* The descriptor has this number of wide characters */ + int src_max = (str.desc.bLength - 1 - 1) / 2; + + /* Neither read nor write more than the smallest buffer */ + int idx_max = MIN(dest_max, src_max); + + int idx; + for (idx = 0; idx < idx_max; ++idx) { + wdata = libusb_le16_to_cpu(str.desc.wData[idx]); + if (wdata < 0x80) + data[idx] = (unsigned char)wdata; + else + data[idx] = '?'; /* non-ASCII */ + } + + data[idx] = 0; /* null-terminate string */ + return idx; +} + +static int parse_iad_array(struct libusb_context *ctx, + struct libusb_interface_association_descriptor_array *iad_array, + const uint8_t *buffer, int size) +{ + uint8_t i; + struct usbi_descriptor_header header; + int consumed = 0; + const uint8_t *buf = buffer; + struct libusb_interface_association_descriptor *iad; + + if (size < LIBUSB_DT_CONFIG_SIZE) { + usbi_err(ctx, "short config descriptor read %d/%d", + size, LIBUSB_DT_CONFIG_SIZE); + return LIBUSB_ERROR_IO; + } + + /* First pass: Iterate through desc list, count number of IADs */ + iad_array->length = 0; + while (size - consumed >= DESC_HEADER_LENGTH) { + header.bLength = buf[0]; + header.bDescriptorType = buf[1]; + if (header.bLength < DESC_HEADER_LENGTH) { + usbi_err(ctx, "invalid descriptor bLength %d", + header.bLength); + return LIBUSB_ERROR_IO; + } + else if (header.bLength > size - consumed) { + usbi_warn(ctx, "short config descriptor read %d/%u", + size - consumed, header.bLength); + return LIBUSB_ERROR_IO; + } + if (header.bDescriptorType == LIBUSB_DT_INTERFACE_ASSOCIATION) + iad_array->length++; + buf += header.bLength; + consumed += header.bLength; + } + + iad_array->iad = NULL; + if (iad_array->length > 0) { + iad = calloc((size_t)iad_array->length, sizeof(*iad)); + if (!iad) + return LIBUSB_ERROR_NO_MEM; + + iad_array->iad = iad; + + /* Second pass: Iterate through desc list, fill IAD structures */ + int remaining = size; + i = 0; + do { + header.bLength = buffer[0]; + header.bDescriptorType = buffer[1]; + if (header.bDescriptorType == LIBUSB_DT_INTERFACE_ASSOCIATION && (remaining >= LIBUSB_DT_INTERFACE_ASSOCIATION_SIZE)) { + iad[i].bLength = buffer[0]; + iad[i].bDescriptorType = buffer[1]; + iad[i].bFirstInterface = buffer[2]; + iad[i].bInterfaceCount = buffer[3]; + iad[i].bFunctionClass = buffer[4]; + iad[i].bFunctionSubClass = buffer[5]; + iad[i].bFunctionProtocol = buffer[6]; + iad[i].iFunction = buffer[7]; + i++; + } + + remaining -= header.bLength; + if (remaining < DESC_HEADER_LENGTH) { + break; + } + buffer += header.bLength; + } while (1); + } + + return LIBUSB_SUCCESS; +} + +static int raw_desc_to_iad_array(struct libusb_context *ctx, const uint8_t *buf, + int size, struct libusb_interface_association_descriptor_array **iad_array) +{ + struct libusb_interface_association_descriptor_array *_iad_array + = calloc(1, sizeof(*_iad_array)); + int r; + + if (!_iad_array) + return LIBUSB_ERROR_NO_MEM; + + r = parse_iad_array(ctx, _iad_array, buf, size); + if (r < 0) { + usbi_err(ctx, "parse_iad_array failed with error %d", r); + free(_iad_array); + return r; + } + + *iad_array = _iad_array; + return LIBUSB_SUCCESS; +} + +/** \ingroup libusb_desc + * Get an array of interface association descriptors (IAD) for a given + * configuration. + * This is a non-blocking function which does not involve any requests being + * sent to the device. + * + * \param dev a device + * \param config_index the index of the configuration you wish to retrieve the + * IADs for. + * \param iad_array output location for the array of IADs. Only valid if 0 was + * returned. Must be freed with libusb_free_interface_association_descriptors() + * after use. It's possible that a given configuration contains no IADs. In this + * case the iad_array is still output, but will have 'length' field set to 0, and + * iad field set to NULL. + * \returns 0 on success + * \returns LIBUSB_ERROR_NOT_FOUND if the configuration does not exist + * \returns another LIBUSB_ERROR code on error + * \see libusb_get_active_interface_association_descriptors() + */ +int API_EXPORTED libusb_get_interface_association_descriptors(libusb_device *dev, + uint8_t config_index, struct libusb_interface_association_descriptor_array **iad_array) +{ + union usbi_config_desc_buf _config; + uint16_t config_len; + uint8_t *buf; + int r; + + if (!iad_array) + return LIBUSB_ERROR_INVALID_PARAM; + + usbi_dbg(DEVICE_CTX(dev), "IADs for config index %u", config_index); + if (config_index >= dev->device_descriptor.bNumConfigurations) + return LIBUSB_ERROR_NOT_FOUND; + + r = get_config_descriptor(dev, config_index, _config.buf, sizeof(_config.buf)); + if (r < 0) + return r; + + config_len = libusb_le16_to_cpu(_config.desc.wTotalLength); + buf = malloc(config_len); + if (!buf) + return LIBUSB_ERROR_NO_MEM; + + r = get_config_descriptor(dev, config_index, buf, config_len); + if (r >= 0) + r = raw_desc_to_iad_array(DEVICE_CTX(dev), buf, r, iad_array); + + free(buf); + return r; +} + +/** \ingroup libusb_desc + * Get an array of interface association descriptors (IAD) for the currently + * active configuration. + * This is a non-blocking function which does not involve any requests being + * sent to the device. + * + * \param dev a device + * \param iad_array output location for the array of IADs. Only valid if 0 was + * returned. Must be freed with libusb_free_interface_association_descriptors() + * after use. It's possible that a given configuration contains no IADs. In this + * case the iad_array is still output, but will have 'length' field set to 0, and + * iad field set to NULL. + * \returns 0 on success + * \returns LIBUSB_ERROR_NOT_FOUND if the device is in unconfigured state + * \returns another LIBUSB_ERROR code on error + * \see libusb_get_interface_association_descriptors + */ +int API_EXPORTED libusb_get_active_interface_association_descriptors(libusb_device *dev, + struct libusb_interface_association_descriptor_array **iad_array) +{ + union usbi_config_desc_buf _config; + uint16_t config_len; + uint8_t *buf; + int r; + + if (!iad_array) + return LIBUSB_ERROR_INVALID_PARAM; + + r = get_active_config_descriptor(dev, _config.buf, sizeof(_config.buf)); + if (r < 0) + return r; + + config_len = libusb_le16_to_cpu(_config.desc.wTotalLength); + buf = malloc(config_len); + if (!buf) + return LIBUSB_ERROR_NO_MEM; + + r = get_active_config_descriptor(dev, buf, config_len); + if (r >= 0) + r = raw_desc_to_iad_array(DEVICE_CTX(dev), buf, r, iad_array); + free(buf); + return r; +} + +/** \ingroup libusb_desc + * Free an array of interface association descriptors (IADs) obtained from + * libusb_get_interface_association_descriptors() or + * libusb_get_active_interface_association_descriptors(). + * It is safe to call this function with a NULL iad_array parameter, in which + * case the function simply returns. + * + * \param iad_array the IAD array to free + */ +void API_EXPORTED libusb_free_interface_association_descriptors( + struct libusb_interface_association_descriptor_array *iad_array) +{ + if (!iad_array) + return; + + if (iad_array->iad) + free((void*)iad_array->iad); + free(iad_array); +} + +/* + * \brief Copy a UTF-8 string with proper truncation if needed. + * + * \param tgt The target utf-8 string. If NULL, then tgt is ignored, + * tgt_size is forced to 0, and this function returns the + * required tgt_size for a subsequent call to this function. + * \param src The source utf-8 string. If NULL, then + * the source string is empty, the return length is 1, + * and, if tgt is not NULL, this function + * sets tgt[0] to the null terminator. + * \param tgt_size The size of target in bytes. + * \return the length of src in bytes, including the null terminator. + * + * utf8_copy(NULL, src, 0) is equivalent to strlen(src) + 1. + */ +static int usbi_utf8_copy(char *tgt, char const *src, int tgt_size) { + uint8_t* t = (uint8_t*)tgt; + uint8_t const* s = (uint8_t const*)src; + + if (NULL == src) { + if ((NULL != tgt) && (tgt_size > 0)) { + tgt[0] = 0; + } + return 1; + } + + if ((NULL == tgt) || (tgt_size <= 0)) { + return (int)(strlen(src) + 1); + } + + // copy UTF-8 string and compute length + int k = 0; + for (k = 0; s[k]; ++k) { + if (k < tgt_size) { + t[k] = s[k]; + } else { + break; + } + } + + if (k >= tgt_size) { + // truncate respecting UTF-8 character boundaries + int idx = tgt_size - 1; + while (idx && (0x80 == (t[idx] & 0xC0))) { // utf-8 continuation byte + --idx; + } + t[idx] = 0; + return (int)(strlen(src) + 1); + } else { + t[k++] = 0; + return k; + } +} + +/** \ingroup libusb_desc + * Retrieve a device string without needing to open the device. + * + * Since version v1.0.30 \ref LIBUSB_API_VERSION >= 0x0100010C + * + * \param dev the target device + * \param string_type the string type to retrieve + * \param data the data buffer for the UTF-8 encoded string. + * \param length the size of the data buffer in bytes. + * USB string descriptors cannot be longer than + * LIBUSB_DEVICE_STRING_BYTES_MAX. + * \returns a negative error code or + * the actual string length in bytes including the null terminator. + * \see libusb_get_string_descriptor() + * \see libusb_get_string_descriptor_ascii() + * + * This function works when the device is still closed since it relies + * on the operating system to provide the string. The operating system + * normally reads and caches the common string descriptors during + * USB enumeration. + * + * Since the USB string descriptor could be processed by the OS, + * this function returns a UTF-8 encoded string. + * + * The string will be returned untranslated or in the default OS language + * when supported by the OS and USB device. + * + * One way to call this function is using a buffer on the stack: + * + * char buffer[LIBUSB_DEVICE_STRING_BYTES_MAX]; + * int ret = libusb_get_device_string(dev, LIBUSB_DEVICE_STRING_SERIAL_NUMBER, buffer, sizeof(buffer)); + * if (ret < 0) { + * // handle error + * } + * + * This function is commonly used to get the serial number to allow + * for device selection before opening the selected device. + */ +int API_EXPORTED libusb_get_device_string(libusb_device *dev, + enum libusb_device_string_type string_type, char *data, int length) { + char * s; + if (NULL == dev) { + return LIBUSB_ERROR_INVALID_PARAM; + } + if ((string_type < 0) || (string_type >= LIBUSB_DEVICE_STRING_COUNT)) { + return LIBUSB_ERROR_INVALID_PARAM; + } + if (length <= 0) { + return LIBUSB_ERROR_INVALID_PARAM; + } + if (NULL == data) { + length = 0; + data = NULL; + } else if (length > 0) { + *data = 0; // return an empty string on errors when possible + } + + if (NULL == dev->device_strings_utf8[string_type]) { + if (usbi_backend.get_device_string) { + s = malloc(LIBUSB_DEVICE_STRING_BYTES_MAX); + int rv = usbi_backend.get_device_string(dev, string_type, s, LIBUSB_DEVICE_STRING_BYTES_MAX); + if (rv < 0) { + free(s); + return rv; + } else { + dev->device_strings_utf8[string_type] = s; + } + } else { + return LIBUSB_ERROR_NOT_SUPPORTED; + } + } + + s = dev->device_strings_utf8[string_type]; + if (NULL == s) { + return LIBUSB_ERROR_NOT_SUPPORTED; + } + + return usbi_utf8_copy(data, s, length); +} diff --git a/usbaudio/src/main/cpp/external/libusb/libusb/hotplug.c b/usbaudio/src/main/cpp/external/libusb/libusb/hotplug.c new file mode 100644 index 000000000..026e11d5a --- /dev/null +++ b/usbaudio/src/main/cpp/external/libusb/libusb/hotplug.c @@ -0,0 +1,489 @@ +/* -*- Mode: C; indent-tabs-mode:t ; c-basic-offset:8 -*- */ +/* + * Hotplug functions for libusb + * Copyright © 2012-2021 Nathan Hjelm + * Copyright © 2012-2013 Peter Stuge + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "libusbi.h" + +/** + * @defgroup libusb_hotplug Device hotplug event notification + * This page details how to use the libusb hotplug interface, where available. + * + * Be mindful that not all platforms currently implement hotplug notification and + * that you should first call on \ref libusb_has_capability() with parameter + * \ref LIBUSB_CAP_HAS_HOTPLUG to confirm that hotplug support is available. + * + * \page libusb_hotplug Device hotplug event notification + * + * \section hotplug_intro Introduction + * + * Version 1.0.16, \ref LIBUSBX_API_VERSION >= 0x01000102, has added support + * for hotplug events on some platforms (you should test if your platform + * supports hotplug notification by calling \ref libusb_has_capability() with + * parameter \ref LIBUSB_CAP_HAS_HOTPLUG). + * + * This interface allows you to request notification for the arrival and departure + * of matching USB devices. + * + * To receive hotplug notification you register a callback by calling + * \ref libusb_hotplug_register_callback(). This function will optionally return + * a callback handle that can be passed to \ref libusb_hotplug_deregister_callback(). + * + * A callback function must return an int (0 or 1) indicating whether the callback is + * expecting additional events. Returning 0 will rearm the callback and 1 will cause + * the callback to be deregistered. Note that when callbacks are called from + * libusb_hotplug_register_callback() because of the \ref LIBUSB_HOTPLUG_ENUMERATE + * flag, the callback return value is ignored. In other words, you cannot cause a + * callback to be deregistered by returning 1 when it is called from + * libusb_hotplug_register_callback(). + * + * Callbacks for a particular context are automatically deregistered by libusb_exit(). + * + * As of 1.0.16 there are two supported hotplug events: + * - LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED: A device has arrived and is ready to use + * - LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT: A device has left and is no longer available + * + * A hotplug event can listen for either or both of these events. + * + * Note: If you receive notification that a device has left and you have any + * libusb_device_handles for the device it is up to you to call libusb_close() + * on each device handle to free up any remaining resources associated with the device. + * Once a device has left any libusb_device_handle associated with the device + * are invalid and will remain so even if the device comes back. + * + * When handling a LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED event it is considered + * safe to call any libusb function that takes a libusb_device. It also safe to + * open a device and submit asynchronous transfers. However, most other functions + * that take a libusb_device_handle are not safe to call. Examples of such + * functions are any of the \ref libusb_syncio "synchronous API" functions or the blocking + * functions that retrieve various \ref libusb_desc "USB descriptors". These functions must + * be used outside of the context of the hotplug callback. + * + * When handling a LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT event the only safe function + * is libusb_get_device_descriptor(). + * + * The following code provides an example of the usage of the hotplug interface: +\code +#include +#include +#include +#include + +static int count = 0; + +int hotplug_callback(struct libusb_context *ctx, struct libusb_device *dev, + libusb_hotplug_event event, void *user_data) { + static libusb_device_handle *dev_handle = NULL; + struct libusb_device_descriptor desc; + int rc; + + (void)libusb_get_device_descriptor(dev, &desc); + + if (LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED == event) { + rc = libusb_open(dev, &dev_handle); + if (LIBUSB_SUCCESS != rc) { + printf("Could not open USB device\n"); + } + } else if (LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT == event) { + if (dev_handle) { + libusb_close(dev_handle); + dev_handle = NULL; + } + } else { + printf("Unhandled event %d\n", event); + } + count++; + + return 0; +} + +int main (void) { + libusb_hotplug_callback_handle callback_handle; + int rc; + + libusb_init_context(NULL, NULL, 0); + + rc = libusb_hotplug_register_callback(NULL, LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED | + LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT, 0, 0x045a, 0x5005, + LIBUSB_HOTPLUG_MATCH_ANY, hotplug_callback, NULL, + &callback_handle); + if (LIBUSB_SUCCESS != rc) { + printf("Error creating a hotplug callback\n"); + libusb_exit(NULL); + return EXIT_FAILURE; + } + + while (count < 2) { + libusb_handle_events_completed(NULL, NULL); + nanosleep(&(struct timespec){0, 10000000UL}, NULL); + } + + libusb_hotplug_deregister_callback(NULL, callback_handle); + libusb_exit(NULL); + + return 0; +} +\endcode + */ + +#define VALID_HOTPLUG_EVENTS \ + (LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED | \ + LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT) + +#define VALID_HOTPLUG_FLAGS \ + (LIBUSB_HOTPLUG_ENUMERATE) + +void usbi_hotplug_init(struct libusb_context *ctx) +{ + /* check for hotplug support */ + if (!libusb_has_capability(LIBUSB_CAP_HAS_HOTPLUG)) + return; + + usbi_mutex_init(&ctx->hotplug_cbs_lock); + list_init(&ctx->hotplug_cbs); + ctx->next_hotplug_cb_handle = 1; + usbi_atomic_store(&ctx->hotplug_ready, 1); +} + +static void usbi_recursively_remove_parents(struct libusb_device *dev, struct libusb_device *next_dev) +{ + if (dev && dev->parent_dev) { + if (usbi_atomic_load(&dev->parent_dev->refcnt) == 1) { + /* The parent was processed before this device in the list and + * therefore has its ref count already decremented for its own ref. + * The only remaining counted ref comes from its remaining single child. + * It will thus be released when its child will be released. So we + * can remove it from the list. This is safe as parent_dev cannot be + * equal to next_dev given that we know at this point that it was + * previously seen in the list. */ + assert (dev->parent_dev != next_dev); + if (dev->parent_dev->list.next && dev->parent_dev->list.prev) { + list_del(&dev->parent_dev->list); + } + } + + usbi_recursively_remove_parents(dev->parent_dev, next_dev); + } +} + +void usbi_hotplug_exit(struct libusb_context *ctx) +{ + struct usbi_hotplug_callback *hotplug_cb, *next_cb; + struct usbi_hotplug_message *msg; + struct libusb_device *dev, *next_dev; + + /* check for hotplug support */ + if (!libusb_has_capability(LIBUSB_CAP_HAS_HOTPLUG)) + return; + + if (!usbi_atomic_load(&ctx->hotplug_ready)) + return; + + /* free all registered hotplug callbacks */ + for_each_hotplug_cb_safe(ctx, hotplug_cb, next_cb) { + list_del(&hotplug_cb->list); + free(hotplug_cb); + } + + /* free all pending hotplug messages */ + while (!list_empty(&ctx->hotplug_msgs)) { + msg = list_first_entry(&ctx->hotplug_msgs, struct usbi_hotplug_message, list); + + /* if the device left, the message holds a reference + * and we must drop it */ + if (msg->event == LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT) + libusb_unref_device(msg->device); + + list_del(&msg->list); + free(msg); + } + + usbi_mutex_lock(&ctx->usb_devs_lock); /* hotplug thread might still be processing an already triggered event, possibly accessing this list as well */ + /* free all discovered devices */ + for_each_device_safe(ctx, dev, next_dev) { + /* remove the device from the usb_devs list only if there are no + * references held, otherwise leave it on the list so that a + * warning message will be shown */ + if (usbi_atomic_load(&dev->refcnt) == 1) { + list_del(&dev->list); + } + + usbi_recursively_remove_parents(dev, next_dev); + + libusb_unref_device(dev); + } + usbi_mutex_unlock(&ctx->usb_devs_lock); + + usbi_mutex_destroy(&ctx->hotplug_cbs_lock); +} + +static int usbi_hotplug_match_cb(struct libusb_device *dev, + libusb_hotplug_event event, struct usbi_hotplug_callback *hotplug_cb) +{ + if (!(hotplug_cb->flags & event)) { + return 0; + } + + if ((hotplug_cb->flags & USBI_HOTPLUG_VENDOR_ID_VALID) && + hotplug_cb->vendor_id != dev->device_descriptor.idVendor) { + return 0; + } + + if ((hotplug_cb->flags & USBI_HOTPLUG_PRODUCT_ID_VALID) && + hotplug_cb->product_id != dev->device_descriptor.idProduct) { + return 0; + } + + if ((hotplug_cb->flags & USBI_HOTPLUG_DEV_CLASS_VALID) && + hotplug_cb->dev_class != dev->device_descriptor.bDeviceClass) { + return 0; + } + + return hotplug_cb->cb(DEVICE_CTX(dev), dev, event, hotplug_cb->user_data); +} + +void usbi_hotplug_notification(struct libusb_context *ctx, struct libusb_device *dev, + libusb_hotplug_event event) +{ + struct usbi_hotplug_message *msg; + unsigned int event_flags; + + /* Only generate a notification if hotplug is ready. This prevents hotplug + * notifications from being generated during initial enumeration or if the + * backend does not support hotplug. */ + if (!usbi_atomic_load(&ctx->hotplug_ready)) + return; + + msg = calloc(1, sizeof(*msg)); + if (!msg) { + usbi_err(ctx, "error allocating hotplug message"); + return; + } + + msg->event = event; + msg->device = dev; + + /* Take the event data lock and add this message to the list. + * Only signal an event if there are no prior pending events. */ + usbi_mutex_lock(&ctx->event_data_lock); + event_flags = ctx->event_flags; + ctx->event_flags |= USBI_EVENT_HOTPLUG_MSG_PENDING; + list_add_tail(&msg->list, &ctx->hotplug_msgs); + if (!event_flags) + usbi_signal_event(&ctx->event); + usbi_mutex_unlock(&ctx->event_data_lock); +} + +void usbi_hotplug_process(struct libusb_context *ctx, struct list_head *hotplug_msgs) +{ + struct usbi_hotplug_callback *hotplug_cb, *next_cb; + struct usbi_hotplug_message *msg; + int r; + + usbi_mutex_lock(&ctx->hotplug_cbs_lock); + + /* dispatch all pending hotplug messages */ + while (!list_empty(hotplug_msgs)) { + msg = list_first_entry(hotplug_msgs, struct usbi_hotplug_message, list); + + for_each_hotplug_cb_safe(ctx, hotplug_cb, next_cb) { + /* skip callbacks that have unregistered */ + if (hotplug_cb->flags & USBI_HOTPLUG_NEEDS_FREE) + continue; + + usbi_mutex_unlock(&ctx->hotplug_cbs_lock); + r = usbi_hotplug_match_cb(msg->device, msg->event, hotplug_cb); + usbi_mutex_lock(&ctx->hotplug_cbs_lock); + + if (r) { + list_del(&hotplug_cb->list); + free(hotplug_cb); + } + } + + /* if the device left, the message holds a reference + * and we must drop it */ + if (msg->event == LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT) + libusb_unref_device(msg->device); + + list_del(&msg->list); + free(msg); + } + + /* free any callbacks that have unregistered */ + for_each_hotplug_cb_safe(ctx, hotplug_cb, next_cb) { + if (hotplug_cb->flags & USBI_HOTPLUG_NEEDS_FREE) { + usbi_dbg(ctx, "freeing hotplug cb %p with handle %d", + (void *) hotplug_cb, hotplug_cb->handle); + list_del(&hotplug_cb->list); + free(hotplug_cb); + } + } + + usbi_mutex_unlock(&ctx->hotplug_cbs_lock); +} + +int API_EXPORTED libusb_hotplug_register_callback(libusb_context *ctx, + int events, int flags, + int vendor_id, int product_id, int dev_class, + libusb_hotplug_callback_fn cb_fn, void *user_data, + libusb_hotplug_callback_handle *callback_handle) +{ + struct usbi_hotplug_callback *hotplug_cb; + + /* check for sane values */ + if (!events || (~VALID_HOTPLUG_EVENTS & events) || + (~VALID_HOTPLUG_FLAGS & flags) || + (LIBUSB_HOTPLUG_MATCH_ANY != vendor_id && (~0xffff & vendor_id)) || + (LIBUSB_HOTPLUG_MATCH_ANY != product_id && (~0xffff & product_id)) || + (LIBUSB_HOTPLUG_MATCH_ANY != dev_class && (~0xff & dev_class)) || + !cb_fn) { + return LIBUSB_ERROR_INVALID_PARAM; + } + + /* check for hotplug support */ + if (!libusb_has_capability(LIBUSB_CAP_HAS_HOTPLUG)) + return LIBUSB_ERROR_NOT_SUPPORTED; + + ctx = usbi_get_context(ctx); + + hotplug_cb = calloc(1, sizeof(*hotplug_cb)); + if (!hotplug_cb) + return LIBUSB_ERROR_NO_MEM; + + hotplug_cb->flags = (uint8_t)events; + if (LIBUSB_HOTPLUG_MATCH_ANY != vendor_id) { + hotplug_cb->flags |= USBI_HOTPLUG_VENDOR_ID_VALID; + hotplug_cb->vendor_id = (uint16_t)vendor_id; + } + if (LIBUSB_HOTPLUG_MATCH_ANY != product_id) { + hotplug_cb->flags |= USBI_HOTPLUG_PRODUCT_ID_VALID; + hotplug_cb->product_id = (uint16_t)product_id; + } + if (LIBUSB_HOTPLUG_MATCH_ANY != dev_class) { + hotplug_cb->flags |= USBI_HOTPLUG_DEV_CLASS_VALID; + hotplug_cb->dev_class = (uint8_t)dev_class; + } + hotplug_cb->cb = cb_fn; + hotplug_cb->user_data = user_data; + + usbi_mutex_lock(&ctx->hotplug_cbs_lock); + + /* protect the handle by the context hotplug lock */ + hotplug_cb->handle = ctx->next_hotplug_cb_handle++; + + /* handle the unlikely case of overflow */ + if (ctx->next_hotplug_cb_handle < 0) + ctx->next_hotplug_cb_handle = 1; + + list_add(&hotplug_cb->list, &ctx->hotplug_cbs); + + usbi_mutex_unlock(&ctx->hotplug_cbs_lock); + + usbi_dbg(ctx, "new hotplug cb %p with handle %d", + (void *) hotplug_cb, hotplug_cb->handle); + + if ((flags & LIBUSB_HOTPLUG_ENUMERATE) && (events & LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED)) { + ssize_t i, len; + struct libusb_device **devs; + + len = libusb_get_device_list(ctx, &devs); + if (len < 0) { + libusb_hotplug_deregister_callback(ctx, hotplug_cb->handle); + return (int)len; + } + + for (i = 0; i < len; i++) { + usbi_hotplug_match_cb(devs[i], + LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED, + hotplug_cb); + } + + libusb_free_device_list(devs, 1); + } + + if (callback_handle) + *callback_handle = hotplug_cb->handle; + + return LIBUSB_SUCCESS; +} + +void API_EXPORTED libusb_hotplug_deregister_callback(libusb_context *ctx, + libusb_hotplug_callback_handle callback_handle) +{ + struct usbi_hotplug_callback *hotplug_cb; + int deregistered = 0; + + /* check for hotplug support */ + if (!libusb_has_capability(LIBUSB_CAP_HAS_HOTPLUG)) + return; + + usbi_dbg(ctx, "deregister hotplug cb %d", callback_handle); + + ctx = usbi_get_context(ctx); + + usbi_mutex_lock(&ctx->hotplug_cbs_lock); + for_each_hotplug_cb(ctx, hotplug_cb) { + if (callback_handle == hotplug_cb->handle) { + /* mark this callback for deregistration */ + hotplug_cb->flags |= USBI_HOTPLUG_NEEDS_FREE; + deregistered = 1; + break; + } + } + usbi_mutex_unlock(&ctx->hotplug_cbs_lock); + + if (deregistered) { + unsigned int event_flags; + + usbi_mutex_lock(&ctx->event_data_lock); + event_flags = ctx->event_flags; + ctx->event_flags |= USBI_EVENT_HOTPLUG_CB_DEREGISTERED; + if (!event_flags) + usbi_signal_event(&ctx->event); + usbi_mutex_unlock(&ctx->event_data_lock); + } +} + +DEFAULT_VISIBILITY +void * LIBUSB_CALL libusb_hotplug_get_user_data(libusb_context *ctx, + libusb_hotplug_callback_handle callback_handle) +{ + struct usbi_hotplug_callback *hotplug_cb; + void *user_data = NULL; + + /* check for hotplug support */ + if (!libusb_has_capability(LIBUSB_CAP_HAS_HOTPLUG)) + return NULL; + + usbi_dbg(ctx, "get hotplug cb %d user data", callback_handle); + + ctx = usbi_get_context(ctx); + + usbi_mutex_lock(&ctx->hotplug_cbs_lock); + for_each_hotplug_cb(ctx, hotplug_cb) { + if (callback_handle == hotplug_cb->handle) { + user_data = hotplug_cb->user_data; + break; + } + } + usbi_mutex_unlock(&ctx->hotplug_cbs_lock); + + return user_data; +} diff --git a/usbaudio/src/main/cpp/external/libusb/libusb/io.c b/usbaudio/src/main/cpp/external/libusb/libusb/io.c new file mode 100644 index 000000000..3dfc6c64b --- /dev/null +++ b/usbaudio/src/main/cpp/external/libusb/libusb/io.c @@ -0,0 +1,2869 @@ +/* -*- Mode: C; indent-tabs-mode:t ; c-basic-offset:8 -*- */ +/* + * I/O functions for libusb + * Copyright © 2007-2009 Daniel Drake + * Copyright © 2001 Johannes Erdfelt + * Copyright © 2019-2022 Nathan Hjelm + * Copyright © 2019-2022 Google LLC. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "libusbi.h" + +/** + * \page libusb_io Synchronous and asynchronous device I/O + * + * \section io_intro Introduction + * + * If you're using libusb in your application, you're probably wanting to + * perform I/O with devices - you want to perform USB data transfers. + * + * libusb offers two separate interfaces for device I/O. This page aims to + * introduce the two in order to help you decide which one is more suitable + * for your application. You can also choose to use both interfaces in your + * application by considering each transfer on a case-by-case basis. + * + * Once you have read through the following discussion, you should consult the + * detailed API documentation pages for the details: + * - \ref libusb_syncio + * - \ref libusb_asyncio + * + * \section theory Transfers at a logical level + * + * At a logical level, USB transfers typically happen in two parts. For + * example, when reading data from a endpoint: + * -# A request for data is sent to the device + * -# Some time later, the incoming data is received by the host + * + * or when writing data to an endpoint: + * + * -# The data is sent to the device + * -# Some time later, the host receives acknowledgement from the device that + * the data has been transferred. + * + * There may be an indefinite delay between the two steps. Consider a + * fictional USB input device with a button that the user can press. In order + * to determine when the button is pressed, you would likely submit a request + * to read data on a bulk or interrupt endpoint and wait for data to arrive. + * Data will arrive when the button is pressed by the user, which is + * potentially hours later. + * + * libusb offers both a synchronous and an asynchronous interface to performing + * USB transfers. The main difference is that the synchronous interface + * combines both steps indicated above into a single function call, whereas + * the asynchronous interface separates them. + * + * \section sync The synchronous interface + * + * The synchronous I/O interface allows you to perform a USB transfer with + * a single function call. When the function call returns, the transfer has + * completed and you can parse the results. + * + * If you have used libusb-0.1 before, this I/O style will seem familiar to + * you. libusb-0.1 only offered a synchronous interface. + * + * In our input device example, to read button presses you might write code + * in the following style: +\code +unsigned char data[4]; +int actual_length; +int r = libusb_bulk_transfer(dev_handle, LIBUSB_ENDPOINT_IN, data, sizeof(data), &actual_length, 0); +if (r == 0 && actual_length == sizeof(data)) { + // results of the transaction can now be found in the data buffer + // parse them here and report button press +} else { + error(); +} +\endcode + * + * The main advantage of this model is simplicity: you did everything with + * a single simple function call. + * + * However, this interface has its limitations. Your application will sleep + * inside libusb_bulk_transfer() until the transaction has completed. If it + * takes the user 3 hours to press the button, your application will be + * sleeping for that long. Execution will be tied up inside the library - + * the entire thread will be useless for that duration. + * + * Another issue is that by tying up the thread with that single transaction + * there is no possibility of performing I/O with multiple endpoints and/or + * multiple devices simultaneously, unless you resort to creating one thread + * per transaction. + * + * Additionally, there is no opportunity to cancel the transfer after the + * request has been submitted. + * + * For details on how to use the synchronous API, see the + * \ref libusb_syncio "synchronous I/O API documentation" pages. + * + * \section async The asynchronous interface + * + * Asynchronous I/O is the most significant new feature in libusb-1.0. + * Although it is a more complex interface, it solves all the issues detailed + * above. + * + * Instead of providing which functions that block until the I/O has complete, + * libusb's asynchronous interface presents non-blocking functions which + * begin a transfer and then return immediately. Your application passes a + * callback function pointer to this non-blocking function, which libusb will + * call with the results of the transaction when it has completed. + * + * Transfers which have been submitted through the non-blocking functions + * can be cancelled with a separate function call. + * + * The non-blocking nature of this interface allows you to be simultaneously + * performing I/O to multiple endpoints on multiple devices, without having + * to use threads. + * + * This added flexibility does come with some complications though: + * - In the interest of being a lightweight library, libusb does not create + * threads and can only operate when your application is calling into it. Your + * application must call into libusb from it's main loop when events are ready + * to be handled, or you must use some other scheme to allow libusb to + * undertake whatever work needs to be done. + * - libusb also needs to be called into at certain fixed points in time in + * order to accurately handle transfer timeouts. + * - Memory handling becomes more complex. You cannot use stack memory unless + * the function with that stack is guaranteed not to return until the transfer + * callback has finished executing. + * - You generally lose some linearity from your code flow because submitting + * the transfer request is done in a separate function from where the transfer + * results are handled. This becomes particularly obvious when you want to + * submit a second transfer based on the results of an earlier transfer. + * + * Internally, libusb's synchronous interface is expressed in terms of function + * calls to the asynchronous interface. + * + * For details on how to use the asynchronous API, see the + * \ref libusb_asyncio "asynchronous I/O API" documentation pages. + */ + + +/** + * \page libusb_packetoverflow Packets and overflows + * + * \section packets Packet abstraction + * + * The USB specifications describe how data is transmitted in packets, with + * constraints on packet size defined by endpoint descriptors. The host must + * not send data payloads larger than the endpoint's maximum packet size. + * + * libusb and the underlying OS abstract out the packet concept, allowing you + * to request transfers of any size. Internally, the request will be divided + * up into correctly-sized packets. You do not have to be concerned with + * packet sizes, but there is one exception when considering overflows. + * + * \section overflow Bulk/interrupt transfer overflows + * + * When requesting data on a bulk endpoint, libusb requires you to supply a + * buffer and the maximum number of bytes of data that libusb can put in that + * buffer. However, the size of the buffer is not communicated to the device - + * the device is just asked to send any amount of data. + * + * There is no problem if the device sends an amount of data that is less than + * or equal to the buffer size. libusb reports this condition to you through + * the \ref libusb_transfer::actual_length "libusb_transfer.actual_length" + * field. + * + * Problems may occur if the device attempts to send more data than can fit in + * the buffer. libusb reports LIBUSB_TRANSFER_OVERFLOW for this condition but + * other behaviour is largely undefined: actual_length may or may not be + * accurate, the chunk of data that can fit in the buffer (before overflow) + * may or may not have been transferred. + * + * Overflows are nasty, but can be avoided. Even though you were told to + * ignore packets above, think about the lower level details: each transfer is + * split into packets (typically small, with a maximum size of 512 bytes). + * Overflows can only happen if the final packet in an incoming data transfer + * is smaller than the actual packet that the device wants to transfer. + * Therefore, you will never see an overflow if your transfer buffer size is a + * multiple of the endpoint's packet size: the final packet will either + * fill up completely or will be only partially filled. + */ + +/** + * @defgroup libusb_asyncio Asynchronous device I/O + * + * This page details libusb's asynchronous (non-blocking) API for USB device + * I/O. This interface is very powerful but is also quite complex - you will + * need to read this page carefully to understand the necessary considerations + * and issues surrounding use of this interface. Simplistic applications + * may wish to consider the \ref libusb_syncio "synchronous I/O API" instead. + * + * The asynchronous interface is built around the idea of separating transfer + * submission and handling of transfer completion (the synchronous model + * combines both of these into one). There may be a long delay between + * submission and completion, however the asynchronous submission function + * is non-blocking so will return control to your application during that + * potentially long delay. + * + * \section asyncabstraction Transfer abstraction + * + * For the asynchronous I/O, libusb implements the concept of a generic + * transfer entity for all types of I/O (control, bulk, interrupt, + * isochronous). The generic transfer object must be treated slightly + * differently depending on which type of I/O you are performing with it. + * + * This is represented by the public libusb_transfer structure type. + * + * \section asynctrf Asynchronous transfers + * + * We can view asynchronous I/O as a 5 step process: + * -# Allocation: allocate a libusb_transfer + * -# Filling: populate the libusb_transfer instance with information + * about the transfer you wish to perform + * -# Submission: ask libusb to submit the transfer + * -# Completion handling: examine transfer results in the + * libusb_transfer structure + * -# Deallocation: clean up resources + * + * + * \subsection asyncalloc Allocation + * + * This step involves allocating memory for a USB transfer. This is the + * generic transfer object mentioned above. At this stage, the transfer + * is "blank" with no details about what type of I/O it will be used for. + * + * Allocation is done with the libusb_alloc_transfer() function. You must use + * this function rather than allocating your own transfers. + * + * \subsection asyncfill Filling + * + * This step is where you take a previously allocated transfer and fill it + * with information to determine the message type and direction, data buffer, + * callback function, etc. + * + * You can either fill the required fields yourself or you can use the + * helper functions: libusb_fill_control_transfer(), libusb_fill_bulk_transfer() + * and libusb_fill_interrupt_transfer(). + * + * \subsection asyncsubmit Submission + * + * When you have allocated a transfer and filled it, you can submit it using + * libusb_submit_transfer(). This function returns immediately but can be + * regarded as firing off the I/O request in the background. + * + * \subsection asynccomplete Completion handling + * + * After a transfer has been submitted, one of four things can happen to it: + * + * - The transfer completes (i.e. some data was transferred) + * - The transfer has a timeout and the timeout expires before all data is + * transferred + * - The transfer fails due to an error + * - The transfer is cancelled + * + * Each of these will cause the user-specified transfer callback function to + * be invoked. It is up to the callback function to determine which of the + * above actually happened and to act accordingly. + * + * The user-specified callback is passed a pointer to the libusb_transfer + * structure which was used to setup and submit the transfer. At completion + * time, libusb has populated this structure with results of the transfer: + * success or failure reason, number of bytes of data transferred, etc. See + * the libusb_transfer structure documentation for more information. + * + * Important Note: The user-specified callback is called from an event + * handling context. It is therefore important that no calls are made into + * libusb that will attempt to perform any event handling. Examples of such + * functions are any listed in the \ref libusb_syncio "synchronous API" and any of + * the blocking functions that retrieve \ref libusb_desc "USB descriptors". + * + * \subsection Deallocation + * + * When a transfer has completed (i.e. the callback function has been invoked), + * you are advised to free the transfer (unless you wish to resubmit it, see + * below). Transfers are deallocated with libusb_free_transfer(). + * + * It is undefined behaviour to free a transfer which has not completed. + * + * \section asyncresubmit Resubmission + * + * You may be wondering why allocation, filling, and submission are all + * separated above where they could reasonably be combined into a single + * operation. + * + * The reason for separation is to allow you to resubmit transfers without + * having to allocate new ones every time. This is especially useful for + * common situations dealing with interrupt endpoints - you allocate one + * transfer, fill and submit it, and when it returns with results you just + * resubmit it for the next interrupt. + * + * \section asynccancel Cancellation + * + * Another advantage of using the asynchronous interface is that you have + * the ability to cancel transfers which have not yet completed. This is + * done by calling the libusb_cancel_transfer() function. + * + * libusb_cancel_transfer() is asynchronous/non-blocking in itself. When the + * cancellation actually completes, the transfer's callback function will + * be invoked, and the callback function should check the transfer status to + * determine that it was cancelled. + * + * On macOS and iOS it is not possible to cancel a single transfer. In this + * case cancelling one transfer on an endpoint will cause all transfers on + * that endpoint to be cancelled. + * + * Freeing the transfer after it has been cancelled but before cancellation + * has completed will result in undefined behaviour. + * + * \attention + * When a transfer is cancelled, some of the data may have been transferred. + * libusb will communicate this to you in the transfer callback. + * Do not assume that no data was transferred. + * + * \section asyncpartial Partial data transfer resulting from cancellation + * + * As noted above, some of the data may have been transferred at the time a + * transfer is cancelled. It is helpful to see how this is possible if you + * consider a bulk transfer to an endpoint with a packet size of 64 bytes. + * Supposing you submit a 512-byte transfer to this endpoint, the operating + * system will divide this transfer up into 8 separate 64-byte frames that the + * host controller will schedule for the device to transfer data. If this + * transfer is cancelled while the device is transferring data, a subset of + * these frames may be descheduled from the host controller before the device + * has the opportunity to finish transferring data to the host. + * + * What your application should do with a partial data transfer is a policy + * decision; there is no single answer that satisfies the needs of every + * application. The data that was successfully transferred should be + * considered entirely valid, but your application must decide what to do with + * the remaining data that was not transferred. Some possible actions to take + * are: + * - Resubmit another transfer for the remaining data, possibly with a shorter + * timeout + * - Discard the partially transferred data and report an error + * + * \section asynctimeout Timeouts + * + * When a transfer times out, libusb internally notes this and attempts to + * cancel the transfer. As noted in \ref asyncpartial "above", it is possible + * that some of the data may actually have been transferred. Your application + * should always check how much data was actually transferred once the + * transfer completes and act accordingly. + * + * \section bulk_overflows Overflows on device-to-host bulk/interrupt endpoints + * + * If your device does not have predictable transfer sizes (or it misbehaves), + * your application may submit a request for data on an IN endpoint which is + * smaller than the data that the device wishes to send. In some circumstances + * this will cause an overflow, which is a nasty condition to deal with. See + * the \ref libusb_packetoverflow page for discussion. + * + * \section asyncctrl Considerations for control transfers + * + * The libusb_transfer structure is generic and hence does not + * include specific fields for the control-specific setup packet structure. + * + * In order to perform a control transfer, you must place the 8-byte setup + * packet at the start of the data buffer. To simplify this, you could + * cast the buffer pointer to type struct libusb_control_setup, or you can + * use the helper function libusb_fill_control_setup(). + * + * The wLength field placed in the setup packet must be the length you would + * expect to be sent in the setup packet: the length of the payload that + * follows (or the expected maximum number of bytes to receive). However, + * the length field of the libusb_transfer object must be the length of + * the data buffer - i.e. it should be wLength plus the size of + * the setup packet (LIBUSB_CONTROL_SETUP_SIZE). + * + * If you use the helper functions, this is simplified for you: + * -# Allocate a buffer of size LIBUSB_CONTROL_SETUP_SIZE plus the size of the + * data you are sending/requesting. + * -# Call libusb_fill_control_setup() on the data buffer, using the transfer + * request size as the wLength value (i.e. do not include the extra space you + * allocated for the control setup). + * -# If this is a host-to-device transfer, place the data to be transferred + * in the data buffer, starting at offset LIBUSB_CONTROL_SETUP_SIZE. + * -# Call libusb_fill_control_transfer() to associate the data buffer with + * the transfer (and to set the remaining details such as callback and timeout). + * - Note that there is no parameter to set the length field of the transfer. + * The length is automatically inferred from the wLength field of the setup + * packet. + * -# Submit the transfer. + * + * The multi-byte control setup fields (wValue, wIndex and wLength) must + * be given in little-endian byte order (the endianness of the USB bus). + * Endianness conversion is transparently handled by + * libusb_fill_control_setup() which is documented to accept host-endian + * values. + * + * Further considerations are needed when handling transfer completion in + * your callback function: + * - As you might expect, the setup packet will still be sitting at the start + * of the data buffer. + * - If this was a device-to-host transfer, the received data will be sitting + * at offset LIBUSB_CONTROL_SETUP_SIZE into the buffer. + * - The actual_length field of the transfer structure is relative to the + * wLength of the setup packet, rather than the size of the data buffer. So, + * if your wLength was 4, your transfer's length was 12, then you + * should expect an actual_length of 4 to indicate that the data was + * transferred in entirety. + * + * To simplify parsing of setup packets and obtaining the data from the + * correct offset, you may wish to use the libusb_control_transfer_get_data() + * and libusb_control_transfer_get_setup() functions within your transfer + * callback. + * + * Even though control endpoints do not halt, a completed control transfer + * may have a LIBUSB_TRANSFER_STALL status code. This indicates the control + * request was not supported. + * + * \section asyncintr Considerations for interrupt transfers + * + * All interrupt transfers are performed using the polling interval presented + * by the bInterval value of the endpoint descriptor. + * + * \section asynciso Considerations for isochronous transfers + * + * Isochronous transfers are more complicated than transfers to + * non-isochronous endpoints. + * + * To perform I/O to an isochronous endpoint, allocate the transfer by calling + * libusb_alloc_transfer() with an appropriate number of isochronous packets. + * + * During filling, set \ref libusb_transfer::type "type" to + * \ref libusb_transfer_type::LIBUSB_TRANSFER_TYPE_ISOCHRONOUS + * "LIBUSB_TRANSFER_TYPE_ISOCHRONOUS", and set + * \ref libusb_transfer::num_iso_packets "num_iso_packets" to a value less than + * or equal to the number of packets you requested during allocation. + * libusb_alloc_transfer() does not set either of these fields for you, given + * that you might not even use the transfer on an isochronous endpoint. + * + * Next, populate the length field for the first num_iso_packets entries in + * the \ref libusb_transfer::iso_packet_desc "iso_packet_desc" array. Section + * 5.6.3 of the USB2 specifications describe how the maximum isochronous + * packet length is determined by the wMaxPacketSize field in the endpoint + * descriptor. + * Two functions can help you here: + * + * - libusb_get_max_iso_packet_size() is an easy way to determine the max + * packet size for an isochronous endpoint. Note that the maximum packet + * size is actually the maximum number of bytes that can be transmitted in + * a single microframe, therefore this function multiplies the maximum number + * of bytes per transaction by the number of transaction opportunities per + * microframe. + * - libusb_set_iso_packet_lengths() assigns the same length to all packets + * within a transfer, which is usually what you want. + * + * For outgoing transfers, you'll obviously fill the buffer and populate the + * packet descriptors in hope that all the data gets transferred. For incoming + * transfers, you must ensure the buffer has sufficient capacity for + * the situation where all packets transfer the full amount of requested data. + * + * Completion handling requires some extra consideration. The + * \ref libusb_transfer::actual_length "actual_length" field of the transfer + * is meaningless and should not be examined; instead you must refer to the + * \ref libusb_iso_packet_descriptor::actual_length "actual_length" field of + * each individual packet. + * + * The \ref libusb_transfer::status "status" field of the transfer is also a + * little misleading: + * - If the packets were submitted and the isochronous data microframes + * completed normally, status will have value + * \ref libusb_transfer_status::LIBUSB_TRANSFER_COMPLETED + * "LIBUSB_TRANSFER_COMPLETED". Note that bus errors and software-incurred + * delays are not counted as transfer errors; the transfer.status field may + * indicate COMPLETED even if some or all of the packets failed. Refer to + * the \ref libusb_iso_packet_descriptor::status "status" field of each + * individual packet to determine packet failures. + * - The status field will have value + * \ref libusb_transfer_status::LIBUSB_TRANSFER_ERROR + * "LIBUSB_TRANSFER_ERROR" only when serious errors were encountered. + * - Other transfer status codes occur with normal behaviour. + * + * The data for each packet will be found at an offset into the buffer that + * can be calculated as if each prior packet completed in full. The + * libusb_get_iso_packet_buffer() and libusb_get_iso_packet_buffer_simple() + * functions may help you here. + * + * \section asynclimits Transfer length limitations + * + * Some operating systems may impose limits on the length of the transfer data + * buffer or, in the case of isochronous transfers, the length of individual + * isochronous packets. Such limits can be difficult for libusb to detect, so + * in most cases the library will simply try and submit the transfer as set up + * by you. If the transfer fails to submit because it is too large, + * libusb_submit_transfer() will return + * \ref libusb_error::LIBUSB_ERROR_INVALID_PARAM "LIBUSB_ERROR_INVALID_PARAM". + * + * The following are known limits for control transfer lengths. Note that this + * length includes the 8-byte setup packet. + * - Linux (4,096 bytes) + * - Windows (4,096 bytes) + * + * \section asyncmem Memory caveats + * + * In most circumstances, it is not safe to use stack memory for transfer + * buffers. This is because the function that fired off the asynchronous + * transfer may return before libusb has finished using the buffer, and when + * the function returns it's stack gets destroyed. This is true for both + * host-to-device and device-to-host transfers. + * + * The only case in which it is safe to use stack memory is where you can + * guarantee that the function owning the stack space for the buffer does not + * return until after the transfer's callback function has completed. In every + * other case, you need to use heap memory instead. + * + * \section asyncflags Fine control + * + * Through using this asynchronous interface, you may find yourself repeating + * a few simple operations many times. You can apply a bitwise OR of certain + * flags to a transfer to simplify certain things: + * - \ref libusb_transfer_flags::LIBUSB_TRANSFER_SHORT_NOT_OK + * "LIBUSB_TRANSFER_SHORT_NOT_OK" results in transfers which transferred + * less than the requested amount of data being marked with status + * \ref libusb_transfer_status::LIBUSB_TRANSFER_ERROR "LIBUSB_TRANSFER_ERROR" + * (they would normally be regarded as COMPLETED) + * - \ref libusb_transfer_flags::LIBUSB_TRANSFER_FREE_BUFFER + * "LIBUSB_TRANSFER_FREE_BUFFER" allows you to ask libusb to free the transfer + * buffer when freeing the transfer. + * - \ref libusb_transfer_flags::LIBUSB_TRANSFER_FREE_TRANSFER + * "LIBUSB_TRANSFER_FREE_TRANSFER" causes libusb to automatically free the + * transfer after the transfer callback returns. + * + * \section asyncevent Event handling + * + * An asynchronous model requires that libusb perform work at various + * points in time - namely processing the results of previously-submitted + * transfers and invoking the user-supplied callback function. + * + * This gives rise to the libusb_handle_events() function which your + * application must call into when libusb has work do to. This gives libusb + * the opportunity to reap pending transfers, invoke callbacks, etc. + * + * \note + * All event handling is performed by whichever thread calls the + * libusb_handle_events() function. libusb does not invoke any callbacks + * outside of this context. Consequently, any callbacks will be run on the + * thread that calls the libusb_handle_events() function. + * + * When to call the libusb_handle_events() function depends on which model + * your application decides to use. The 2 different approaches: + * + * -# Repeatedly call libusb_handle_events() in blocking mode from a dedicated + * thread. + * -# Integrate libusb with your application's main event loop. libusb + * exposes a set of file descriptors which allow you to do this. + * + * The first approach has the big advantage that it will also work on Windows + * were libusb' poll API for select / poll integration is not available. So + * if you want to support Windows and use the async API, you must use this + * approach, see the \ref eventthread "Using an event handling thread" section + * below for details. + * + * If you prefer a single threaded approach with a single central event loop, + * see the \ref libusb_poll "polling and timing" section for how to integrate libusb + * into your application's main event loop. + * + * \section eventthread Using an event handling thread + * + * Lets begin with stating the obvious: If you're going to use a separate + * thread for libusb event handling, your callback functions MUST be + * thread-safe. + * + * Other then that doing event handling from a separate thread, is mostly + * simple. You can use an event thread function as follows: +\code +void *event_thread_func(void *ctx) +{ + while (event_thread_run) + libusb_handle_events(ctx); + + return NULL; +} +\endcode + * + * There is one caveat though, stopping this thread requires setting the + * event_thread_run variable to 0, and after that libusb_handle_events() needs + * to return control to event_thread_func. But unless some event happens, + * libusb_handle_events() will not return. + * + * There are 2 different ways of dealing with this, depending on if your + * application uses libusb' \ref libusb_hotplug "hotplug" support or not. + * + * Applications which do not use hotplug support, should not start the event + * thread until after their first call to libusb_open(), and should stop the + * thread when closing the last open device as follows: +\code +void my_close_handle(libusb_device_handle *dev_handle) +{ + if (open_devs == 1) + event_thread_run = 0; + + libusb_close(dev_handle); // This wakes up libusb_handle_events() + + if (open_devs == 1) + pthread_join(event_thread); + + open_devs--; +} +\endcode + * + * Applications using hotplug support should start the thread at program init, + * after having successfully called libusb_hotplug_register_callback(), and + * should stop the thread at program exit as follows: +\code +void my_libusb_exit(void) +{ + event_thread_run = 0; + libusb_hotplug_deregister_callback(ctx, hotplug_cb_handle); // This wakes up libusb_handle_events() + pthread_join(event_thread); + libusb_exit(ctx); +} +\endcode + */ + +/** + * @defgroup libusb_poll Polling and timing + * + * This page documents libusb's functions for polling events and timing. + * These functions are only necessary for users of the + * \ref libusb_asyncio "asynchronous API". If you are only using the simpler + * \ref libusb_syncio "synchronous API" then you do not need to ever call these + * functions. + * + * The justification for the functionality described here has already been + * discussed in the \ref asyncevent "event handling" section of the + * asynchronous API documentation. In summary, libusb does not create internal + * threads for event processing and hence relies on your application calling + * into libusb at certain points in time so that pending events can be handled. + * + * Your main loop is probably already calling poll() or select() or a + * variant on a set of file descriptors for other event sources (e.g. keyboard + * button presses, mouse movements, network sockets, etc). You then add + * libusb's file descriptors to your poll()/select() calls, and when activity + * is detected on such descriptors you know it is time to call + * libusb_handle_events(). + * + * There is one final event handling complication. libusb supports + * asynchronous transfers which time out after a specified time period. + * + * On some platforms a timerfd is used, so the timeout handling is just another + * fd, on other platforms this requires that libusb is called into at or after + * the timeout to handle it. So, in addition to considering libusb's file + * descriptors in your main event loop, you must also consider that libusb + * sometimes needs to be called into at fixed points in time even when there + * is no file descriptor activity, see \ref polltime details. + * + * In order to know precisely when libusb needs to be called into, libusb + * offers you a set of pollable file descriptors and information about when + * the next timeout expires. + * + * If you are using the asynchronous I/O API, you must take one of the two + * following options, otherwise your I/O will not complete. + * + * \section pollsimple The simple option + * + * If your application revolves solely around libusb and does not need to + * handle other event sources, you can have a program structure as follows: +\code +// initialize libusb +// find and open device +// maybe fire off some initial async I/O + +while (user_has_not_requested_exit) + libusb_handle_events(ctx); + +// clean up and exit +\endcode + * + * With such a simple main loop, you do not have to worry about managing + * sets of file descriptors or handling timeouts. libusb_handle_events() will + * handle those details internally. + * + * \section libusb_pollmain The more advanced option + * + * \note This functionality is currently only available on Unix-like platforms. + * On Windows, libusb_get_pollfds() simply returns NULL. Applications which + * want to support Windows are advised to use an \ref eventthread + * "event handling thread" instead. + * + * In more advanced applications, you will already have a main loop which + * is monitoring other event sources: network sockets, X11 events, mouse + * movements, etc. Through exposing a set of file descriptors, libusb is + * designed to cleanly integrate into such main loops. + * + * In addition to polling file descriptors for the other event sources, you + * take a set of file descriptors from libusb and monitor those too. When you + * detect activity on libusb's file descriptors, you call + * libusb_handle_events_timeout() in non-blocking mode. + * + * What's more, libusb may also need to handle events at specific moments in + * time. No file descriptor activity is generated at these times, so your + * own application needs to be continually aware of when the next one of these + * moments occurs (through calling libusb_get_next_timeout()), and then it + * needs to call libusb_handle_events_timeout() in non-blocking mode when + * these moments occur. This means that you need to adjust your + * poll()/select() timeout accordingly. + * + * libusb provides you with a set of file descriptors to poll and expects you + * to poll all of them, treating them as a single entity. The meaning of each + * file descriptor in the set is an internal implementation detail, + * platform-dependent and may vary from release to release. Don't try and + * interpret the meaning of the file descriptors, just do as libusb indicates, + * polling all of them at once. + * + * In pseudo-code, you want something that looks like: +\code +// initialise libusb + +libusb_get_pollfds(ctx) +while (user has not requested application exit) { + libusb_get_next_timeout(ctx); + poll(on libusb file descriptors plus any other event sources of interest, + using a timeout no larger than the value libusb just suggested) + if (poll() indicated activity on libusb file descriptors) + libusb_handle_events_timeout(ctx, &zero_tv); + if (time has elapsed to or beyond the libusb timeout) + libusb_handle_events_timeout(ctx, &zero_tv); + // handle events from other sources here +} + +// clean up and exit +\endcode + * + * \subsection polltime Notes on time-based events + * + * The above complication with having to track time and call into libusb at + * specific moments is a bit of a headache. For maximum compatibility, you do + * need to write your main loop as above, but you may decide that you can + * restrict the supported platforms of your application and get away with + * a more simplistic scheme. + * + * These time-based event complications are \b not required on the following + * platforms: + * - Darwin + * - Linux, provided that the following version requirements are satisfied: + * - Linux v2.6.27 or newer, compiled with timerfd support + * - glibc v2.9 or newer + * - libusb v1.0.5 or newer + * + * Under these configurations, libusb_get_next_timeout() will \em always return + * 0, so your main loop can be simplified to: +\code +// initialise libusb + +libusb_get_pollfds(ctx) +while (user has not requested application exit) { + poll(on libusb file descriptors plus any other event sources of interest, + using any timeout that you like) + if (poll() indicated activity on libusb file descriptors) + libusb_handle_events_timeout(ctx, &zero_tv); + // handle events from other sources here +} + +// clean up and exit +\endcode + * + * Do remember that if you simplify your main loop to the above, you will + * lose compatibility with some platforms (including legacy Linux platforms, + * and any future platforms supported by libusb which may have time-based + * event requirements). The resultant problems will likely appear as + * strange bugs in your application. + * + * You can use the libusb_pollfds_handle_timeouts() function to do a runtime + * check to see if it is safe to ignore the time-based event complications. + * If your application has taken the shortcut of ignoring libusb's next timeout + * in your main loop, then you are advised to check the return value of + * libusb_pollfds_handle_timeouts() during application startup, and to abort + * if the platform does suffer from these timing complications. + * + * \subsection fdsetchange Changes in the file descriptor set + * + * The set of file descriptors that libusb uses as event sources may change + * during the life of your application. Rather than having to repeatedly + * call libusb_get_pollfds(), you can set up notification functions for when + * the file descriptor set changes using libusb_set_pollfd_notifiers(). + * + * \subsection mtissues Multi-threaded considerations + * + * Unfortunately, the situation is complicated further when multiple threads + * come into play. If two threads are monitoring the same file descriptors, + * the fact that only one thread will be woken up when an event occurs causes + * some headaches. + * + * The events lock, event waiters lock, and libusb_handle_events_locked() + * entities are added to solve these problems. You do not need to be concerned + * with these entities otherwise. + * + * See the extra documentation: \ref libusb_mtasync + */ + +/** \page libusb_mtasync Multi-threaded applications and asynchronous I/O + * + * libusb is a thread-safe library, but extra considerations must be applied + * to applications which interact with libusb from multiple threads. + * + * The underlying issue that must be addressed is that all libusb I/O + * revolves around monitoring file descriptors through the poll()/select() + * system calls. This is directly exposed at the + * \ref libusb_asyncio "asynchronous interface" but it is important to note that the + * \ref libusb_syncio "synchronous interface" is implemented on top of the + * asynchronous interface, therefore the same considerations apply. + * + * The issue is that if two or more threads are concurrently calling poll() + * or select() on libusb's file descriptors then only one of those threads + * will be woken up when an event arrives. The others will be completely + * oblivious that anything has happened. + * + * Consider the following pseudo-code, which submits an asynchronous transfer + * then waits for its completion. This style is one way you could implement a + * synchronous interface on top of the asynchronous interface (and libusb + * does something similar, albeit more advanced due to the complications + * explained on this page). + * +\code +void cb(struct libusb_transfer *transfer) +{ + int *completed = transfer->user_data; + *completed = 1; +} + +void myfunc() { + struct libusb_transfer *transfer; + unsigned char buffer[LIBUSB_CONTROL_SETUP_SIZE] __attribute__ ((aligned (2))); + int completed = 0; + + transfer = libusb_alloc_transfer(0); + libusb_fill_control_setup(buffer, + LIBUSB_REQUEST_TYPE_VENDOR | LIBUSB_ENDPOINT_OUT, 0x04, 0x01, 0, 0); + libusb_fill_control_transfer(transfer, dev, buffer, cb, &completed, 1000); + libusb_submit_transfer(transfer); + + while (!completed) { + poll(libusb file descriptors, 120*1000); + if (poll indicates activity) + libusb_handle_events_timeout(ctx, &zero_tv); + } + printf("completed!"); + // other code here +} +\endcode + * + * Here we are serializing completion of an asynchronous event + * against a condition - the condition being completion of a specific transfer. + * The poll() loop has a long timeout to minimize CPU usage during situations + * when nothing is happening (it could reasonably be unlimited). + * + * If this is the only thread that is polling libusb's file descriptors, there + * is no problem: there is no danger that another thread will swallow up the + * event that we are interested in. On the other hand, if there is another + * thread polling the same descriptors, there is a chance that it will receive + * the event that we were interested in. In this situation, myfunc() + * will only realise that the transfer has completed on the next iteration of + * the loop, up to 120 seconds later. Clearly a two-minute delay is + * undesirable, and don't even think about using short timeouts to circumvent + * this issue! + * + * The solution here is to ensure that no two threads are ever polling the + * file descriptors at the same time. A naive implementation of this would + * impact the capabilities of the library, so libusb offers the scheme + * documented below to ensure no loss of functionality. + * + * Before we go any further, it is worth mentioning that all libusb-wrapped + * event handling procedures fully adhere to the scheme documented below. + * This includes libusb_handle_events() and its variants, and all the + * synchronous I/O functions - libusb hides this headache from you. + * + * \section Using libusb_handle_events() from multiple threads + * + * Even when only using libusb_handle_events() and synchronous I/O functions, + * you can still have a race condition. You might be tempted to solve the + * above with libusb_handle_events() like so: + * +\code + libusb_submit_transfer(transfer); + + while (!completed) { + libusb_handle_events(ctx); + } + printf("completed!"); +\endcode + * + * This however has a race between the checking of completed and + * libusb_handle_events() acquiring the events lock, so another thread + * could have completed the transfer, resulting in this thread hanging + * until either a timeout or another event occurs. See also commit + * 6696512aade99bb15d6792af90ae329af270eba6 which fixes this in the + * synchronous API implementation of libusb. + * + * Fixing this race requires checking the variable completed only after + * taking the event lock, which defeats the concept of just calling + * libusb_handle_events() without worrying about locking. This is why + * libusb-1.0.9 introduces the new libusb_handle_events_timeout_completed() + * and libusb_handle_events_completed() functions, which handles doing the + * completion check for you after they have acquired the lock: + * +\code + libusb_submit_transfer(transfer); + + while (!completed) { + libusb_handle_events_completed(ctx, &completed); + } + printf("completed!"); +\endcode + * + * This nicely fixes the race in our example. Note that if all you want to + * do is submit a single transfer and wait for its completion, then using + * one of the synchronous I/O functions is much easier. + * + * \note + * The `completed` variable must be modified while holding the event lock, + * otherwise a race condition can still exist. It is simplest to do so from + * within the transfer callback as shown above. + * + * \section eventlock The events lock + * + * The problem is when we consider the fact that libusb exposes file + * descriptors to allow for you to integrate asynchronous USB I/O into + * existing main loops, effectively allowing you to do some work behind + * libusb's back. If you do take libusb's file descriptors and pass them to + * poll()/select() yourself, you need to be aware of the associated issues. + * + * The first concept to be introduced is the events lock. The events lock + * is used to serialize threads that want to handle events, such that only + * one thread is handling events at any one time. + * + * You must take the events lock before polling libusb file descriptors, + * using libusb_lock_events(). You must release the lock as soon as you have + * aborted your poll()/select() loop, using libusb_unlock_events(). + * + * \section threadwait Letting other threads do the work for you + * + * Although the events lock is a critical part of the solution, it is not + * enough on it's own. You might wonder if the following is sufficient... +\code + libusb_lock_events(ctx); + while (!completed) { + poll(libusb file descriptors, 120*1000); + if (poll indicates activity) + libusb_handle_events_timeout(ctx, &zero_tv); + } + libusb_unlock_events(ctx); +\endcode + * ...and the answer is that it is not. This is because the transfer in the + * code shown above may take a long time (say 30 seconds) to complete, and + * the lock is not released until the transfer is completed. + * + * Another thread with similar code that wants to do event handling may be + * working with a transfer that completes after a few milliseconds. Despite + * having such a quick completion time, the other thread cannot check that + * status of its transfer until the code above has finished (30 seconds later) + * due to contention on the lock. + * + * To solve this, libusb offers you a mechanism to determine when another + * thread is handling events. It also offers a mechanism to block your thread + * until the event handling thread has completed an event (and this mechanism + * does not involve polling of file descriptors). + * + * After determining that another thread is currently handling events, you + * obtain the event waiters lock using libusb_lock_event_waiters(). + * You then re-check that some other thread is still handling events, and if + * so, you call libusb_wait_for_event(). + * + * libusb_wait_for_event() puts your application to sleep until an event + * occurs, or until a thread releases the events lock. When either of these + * things happen, your thread is woken up, and should re-check the condition + * it was waiting on. It should also re-check that another thread is handling + * events, and if not, it should start handling events itself. + * + * This looks like the following, as pseudo-code: +\code +retry: +if (libusb_try_lock_events(ctx) == 0) { + // we obtained the event lock: do our own event handling + while (!completed) { + if (!libusb_event_handling_ok(ctx)) { + libusb_unlock_events(ctx); + goto retry; + } + poll(libusb file descriptors, 120*1000); + if (poll indicates activity) + libusb_handle_events_locked(ctx, 0); + } + libusb_unlock_events(ctx); +} else { + // another thread is doing event handling. wait for it to signal us that + // an event has completed + libusb_lock_event_waiters(ctx); + + while (!completed) { + // now that we have the event waiters lock, double check that another + // thread is still handling events for us. (it may have ceased handling + // events in the time it took us to reach this point) + if (!libusb_event_handler_active(ctx)) { + // whoever was handling events is no longer doing so, try again + libusb_unlock_event_waiters(ctx); + goto retry; + } + + libusb_wait_for_event(ctx, NULL); + } + libusb_unlock_event_waiters(ctx); +} +printf("completed!\n"); +\endcode + * + * A naive look at the above code may suggest that this can only support + * one event waiter (hence a total of 2 competing threads, the other doing + * event handling), because the event waiter seems to have taken the event + * waiters lock while waiting for an event. However, the system does support + * multiple event waiters, because libusb_wait_for_event() actually drops + * the lock while waiting, and reacquires it before continuing. + * + * We have now implemented code which can dynamically handle situations where + * nobody is handling events (so we should do it ourselves), and it can also + * handle situations where another thread is doing event handling (so we can + * piggyback onto them). It is also equipped to handle a combination of + * the two, for example, another thread is doing event handling, but for + * whatever reason it stops doing so before our condition is met, so we take + * over the event handling. + * + * Four functions were introduced in the above pseudo-code. Their importance + * should be apparent from the code shown above. + * -# libusb_try_lock_events() is a non-blocking function which attempts + * to acquire the events lock but returns a failure code if it is contended. + * -# libusb_event_handling_ok() checks that libusb is still happy for your + * thread to be performing event handling. Sometimes, libusb needs to + * interrupt the event handler, and this is how you can check if you have + * been interrupted. If this function returns 0, the correct behaviour is + * for you to give up the event handling lock, and then to repeat the cycle. + * The following libusb_try_lock_events() will fail, so you will become an + * events waiter. For more information on this, read \ref fullstory below. + * -# libusb_handle_events_locked() is a variant of + * libusb_handle_events_timeout() that you can call while holding the + * events lock. libusb_handle_events_timeout() itself implements similar + * logic to the above, so be sure not to call it when you are + * "working behind libusb's back", as is the case here. + * -# libusb_event_handler_active() determines if someone is currently + * holding the events lock + * + * You might be wondering why there is no function to wake up all threads + * blocked on libusb_wait_for_event(). This is because libusb can do this + * internally: it will wake up all such threads when someone calls + * libusb_unlock_events() or when a transfer completes (at the point after its + * callback has returned). + * + * \subsection fullstory The full story + * + * The above explanation should be enough to get you going, but if you're + * really thinking through the issues then you may be left with some more + * questions regarding libusb's internals. If you're curious, read on, and if + * not, skip to the next section to avoid confusing yourself! + * + * The immediate question that may spring to mind is: what if one thread + * modifies the set of file descriptors that need to be polled while another + * thread is doing event handling? + * + * There are 2 situations in which this may happen. + * -# libusb_open() will add another file descriptor to the poll set, + * therefore it is desirable to interrupt the event handler so that it + * restarts, picking up the new descriptor. + * -# libusb_close() will remove a file descriptor from the poll set. There + * are all kinds of race conditions that could arise here, so it is + * important that nobody is doing event handling at this time. + * + * libusb handles these issues internally, so application developers do not + * have to stop their event handlers while opening/closing devices. Here's how + * it works, focusing on the libusb_close() situation first: + * + * -# During initialization, libusb opens an internal pipe, and it adds the read + * end of this pipe to the set of file descriptors to be polled. + * -# During libusb_close(), libusb writes some dummy data on this event pipe. + * This immediately interrupts the event handler. libusb also records + * internally that it is trying to interrupt event handlers for this + * high-priority event. + * -# At this point, some of the functions described above start behaving + * differently: + * - libusb_event_handling_ok() starts returning 1, indicating that it is NOT + * OK for event handling to continue. + * - libusb_try_lock_events() starts returning 1, indicating that another + * thread holds the event handling lock, even if the lock is uncontended. + * - libusb_event_handler_active() starts returning 1, indicating that + * another thread is doing event handling, even if that is not true. + * -# The above changes in behaviour result in the event handler stopping and + * giving up the events lock very quickly, giving the high-priority + * libusb_close() operation a "free ride" to acquire the events lock. All + * threads that are competing to do event handling become event waiters. + * -# With the events lock held inside libusb_close(), libusb can safely remove + * a file descriptor from the poll set, in the safety of knowledge that + * nobody is polling those descriptors or trying to access the poll set. + * -# After obtaining the events lock, the close operation completes very + * quickly (usually a matter of milliseconds) and then immediately releases + * the events lock. + * -# At the same time, the behaviour of libusb_event_handling_ok() and friends + * reverts to the original, documented behaviour. + * -# The release of the events lock causes the threads that are waiting for + * events to be woken up and to start competing to become event handlers + * again. One of them will succeed; it will then re-obtain the list of poll + * descriptors, and USB I/O will then continue as normal. + * + * libusb_open() is similar, and is actually a more simplistic case. Upon a + * call to libusb_open(): + * + * -# The device is opened and a file descriptor is added to the poll set. + * -# libusb sends some dummy data on the event pipe, and records that it + * is trying to modify the poll descriptor set. + * -# The event handler is interrupted, and the same behaviour change as for + * libusb_close() takes effect, causing all event handling threads to become + * event waiters. + * -# The libusb_open() implementation takes its free ride to the events lock. + * -# Happy that it has successfully paused the events handler, libusb_open() + * releases the events lock. + * -# The event waiter threads are all woken up and compete to become event + * handlers again. The one that succeeds will obtain the list of poll + * descriptors again, which will include the addition of the new device. + * + * \subsection concl Closing remarks + * + * The above may seem a little complicated, but hopefully I have made it clear + * why such complications are necessary. Also, do not forget that this only + * applies to applications that take libusb's file descriptors and integrate + * them into their own polling loops. + * + * You may decide that it is OK for your multi-threaded application to ignore + * some of the rules and locks detailed above, because you don't think that + * two threads can ever be polling the descriptors at the same time. If that + * is the case, then that's good news for you because you don't have to worry. + * But be careful here; remember that the synchronous I/O functions do event + * handling internally. If you have one thread doing event handling in a loop + * (without implementing the rules and locking semantics documented above) + * and another trying to send a synchronous USB transfer, you will end up with + * two threads monitoring the same descriptors, and the above-described + * undesirable behaviour occurring. The solution is for your polling thread to + * play by the rules; the synchronous I/O functions do so, and this will result + * in them getting along in perfect harmony. + * + * If you do have a dedicated thread doing event handling, it is perfectly + * legal for it to take the event handling lock for long periods of time. Any + * synchronous I/O functions you call from other threads will transparently + * fall back to the "event waiters" mechanism detailed above. The only + * consideration that your event handling thread must apply is the one related + * to libusb_event_handling_ok(): you must call this before every poll(), and + * give up the events lock if instructed. + */ + +int usbi_io_init(struct libusb_context *ctx) +{ + int r; + + usbi_mutex_init(&ctx->flying_transfers_lock); + usbi_mutex_init(&ctx->events_lock); + usbi_mutex_init(&ctx->event_waiters_lock); + usbi_cond_init(&ctx->event_waiters_cond); + usbi_mutex_init(&ctx->event_data_lock); + usbi_tls_key_create(&ctx->event_handling_key); + list_init(&ctx->flying_transfers); + list_init(&ctx->event_sources); + list_init(&ctx->removed_event_sources); + list_init(&ctx->hotplug_msgs); + list_init(&ctx->completed_transfers); + + r = usbi_create_event(&ctx->event); + if (r < 0) + goto err; + + r = usbi_add_event_source(ctx, USBI_EVENT_OS_HANDLE(&ctx->event), USBI_EVENT_POLL_EVENTS); + if (r < 0) + goto err_destroy_event; + +#ifdef HAVE_OS_TIMER + r = usbi_create_timer(&ctx->timer); + if (r == 0) { + usbi_dbg(ctx, "using timer for timeouts"); + r = usbi_add_event_source(ctx, USBI_TIMER_OS_HANDLE(&ctx->timer), USBI_TIMER_POLL_EVENTS); + if (r < 0) + goto err_destroy_timer; + } else { + usbi_dbg(ctx, "timer not available for timeouts"); + } +#endif + + return 0; + +#ifdef HAVE_OS_TIMER +err_destroy_timer: + usbi_destroy_timer(&ctx->timer); + usbi_remove_event_source(ctx, USBI_EVENT_OS_HANDLE(&ctx->event)); +#endif +err_destroy_event: + usbi_destroy_event(&ctx->event); +err: + usbi_mutex_destroy(&ctx->flying_transfers_lock); + usbi_mutex_destroy(&ctx->events_lock); + usbi_mutex_destroy(&ctx->event_waiters_lock); + usbi_cond_destroy(&ctx->event_waiters_cond); + usbi_mutex_destroy(&ctx->event_data_lock); + usbi_tls_key_delete(ctx->event_handling_key); + return r; +} + +static void cleanup_removed_event_sources(struct libusb_context *ctx) +{ + struct usbi_event_source *ievent_source, *tmp; + + for_each_removed_event_source_safe(ctx, ievent_source, tmp) { + list_del(&ievent_source->list); + free(ievent_source); + } +} + +void usbi_io_exit(struct libusb_context *ctx) +{ +#ifdef HAVE_OS_TIMER + if (usbi_using_timer(ctx)) { + usbi_remove_event_source(ctx, USBI_TIMER_OS_HANDLE(&ctx->timer)); + usbi_destroy_timer(&ctx->timer); + } +#endif + usbi_remove_event_source(ctx, USBI_EVENT_OS_HANDLE(&ctx->event)); + usbi_destroy_event(&ctx->event); + usbi_mutex_destroy(&ctx->flying_transfers_lock); + usbi_mutex_destroy(&ctx->events_lock); + usbi_mutex_destroy(&ctx->event_waiters_lock); + usbi_cond_destroy(&ctx->event_waiters_cond); + usbi_mutex_destroy(&ctx->event_data_lock); + usbi_tls_key_delete(ctx->event_handling_key); + cleanup_removed_event_sources(ctx); + free(ctx->event_data); +} + +static void calculate_timeout(struct usbi_transfer *itransfer) +{ + struct libusb_transfer *transfer = USBI_TRANSFER_TO_LIBUSB_TRANSFER(itransfer); + unsigned int timeout = transfer->timeout; + + if (!timeout) { + TIMESPEC_CLEAR(&itransfer->timeout); + return; + } + + usbi_get_monotonic_time(&itransfer->timeout); + + itransfer->timeout.tv_sec += timeout / 1000U; + itransfer->timeout.tv_nsec += (timeout % 1000U) * 1000000L; + if (itransfer->timeout.tv_nsec >= NSEC_PER_SEC) { + ++itransfer->timeout.tv_sec; + itransfer->timeout.tv_nsec -= NSEC_PER_SEC; + } +} + +/** \ingroup libusb_asyncio + * Allocate a libusb transfer with a specified number of isochronous packet + * descriptors. The returned transfer is pre-initialized for you. When the new + * transfer is no longer needed, it should be freed with + * libusb_free_transfer(). + * + * Transfers intended for non-isochronous endpoints (e.g. control, bulk, + * interrupt) should specify an iso_packets count of zero. + * + * For transfers intended for isochronous endpoints, specify an appropriate + * number of packet descriptors to be allocated as part of the transfer. + * The returned transfer is not specially initialized for isochronous I/O; + * you are still required to set the + * \ref libusb_transfer::num_iso_packets "num_iso_packets" and + * \ref libusb_transfer::type "type" fields accordingly. + * + * It is safe to allocate a transfer with some isochronous packets and then + * use it on a non-isochronous endpoint. If you do this, ensure that at time + * of submission, num_iso_packets is 0 and that type is set appropriately. + * + * \param iso_packets number of isochronous packet descriptors to allocate. Must be non-negative. + * \returns a newly allocated transfer, or NULL on error + */ +DEFAULT_VISIBILITY +struct libusb_transfer * LIBUSB_CALL libusb_alloc_transfer( + int iso_packets) +{ + assert(iso_packets >= 0); + if (iso_packets < 0) + return NULL; + + size_t priv_size = PTR_ALIGN(usbi_backend.transfer_priv_size); + size_t usbi_transfer_size = PTR_ALIGN(sizeof(struct usbi_transfer)); + size_t libusb_transfer_size = PTR_ALIGN(sizeof(struct libusb_transfer)); + size_t iso_packets_size = sizeof(struct libusb_iso_packet_descriptor) * (size_t)iso_packets; + size_t alloc_size = priv_size + usbi_transfer_size + libusb_transfer_size + iso_packets_size; + unsigned char *ptr = calloc(1, alloc_size); + if (!ptr) + return NULL; + + struct usbi_transfer *itransfer = (struct usbi_transfer *)(ptr + priv_size); + itransfer->priv = ptr; + usbi_mutex_init(&itransfer->lock); + struct libusb_transfer *transfer = USBI_TRANSFER_TO_LIBUSB_TRANSFER(itransfer); + + return transfer; +} + +/** \ingroup libusb_asyncio + * Free a transfer structure. This should be called for all transfers + * allocated with libusb_alloc_transfer(). + * + * If the \ref libusb_transfer_flags::LIBUSB_TRANSFER_FREE_BUFFER + * "LIBUSB_TRANSFER_FREE_BUFFER" flag is set and the transfer buffer is + * non-NULL, this function will also free the transfer buffer using the + * standard system memory allocator (e.g. free()). + * + * It is legal to call this function with a NULL transfer. In this case, + * the function will simply return safely. + * + * It is not legal to free an active transfer (one which has been submitted + * and has not yet completed). + * + * \param transfer the transfer to free + */ +void API_EXPORTED libusb_free_transfer(struct libusb_transfer *transfer) +{ + if (!transfer) + return; + + usbi_dbg(TRANSFER_CTX(transfer), "transfer %p", (void *) transfer); + if (transfer->flags & LIBUSB_TRANSFER_FREE_BUFFER) + free(transfer->buffer); + + struct usbi_transfer *itransfer = LIBUSB_TRANSFER_TO_USBI_TRANSFER(transfer); + usbi_mutex_destroy(&itransfer->lock); + if (itransfer->dev) + libusb_unref_device(itransfer->dev); + + unsigned char *ptr = USBI_TRANSFER_TO_TRANSFER_PRIV(itransfer); + assert(ptr == itransfer->priv); + free(ptr); +} + +/* iterates through the flying transfers, and rearms the timer based on the + * next upcoming timeout. + * NB: flying_transfers_lock must be held when calling this. + * returns 0 on success or a LIBUSB_ERROR code on failure. + */ +#ifdef HAVE_OS_TIMER +static int arm_timer_for_next_timeout(struct libusb_context *ctx) +{ + struct usbi_transfer *itransfer; + + if (!usbi_using_timer(ctx)) + return 0; + + for_each_transfer(ctx, itransfer) { + struct timespec *cur_ts = &itransfer->timeout; + + /* if we've reached transfers of infinite timeout, then we have no + * arming to do */ + if (!TIMESPEC_IS_SET(cur_ts)) + break; + + /* act on first transfer that has not already been handled */ + if (!(itransfer->timeout_flags & (USBI_TRANSFER_TIMEOUT_HANDLED | USBI_TRANSFER_OS_HANDLES_TIMEOUT))) { +#ifdef ENABLE_LOGGING + struct libusb_transfer *transfer = USBI_TRANSFER_TO_LIBUSB_TRANSFER(itransfer); + usbi_dbg(ctx, "next timeout originally %ums", transfer->timeout); +#endif + return usbi_arm_timer(&ctx->timer, cur_ts); + } + } + + usbi_dbg(ctx, "no timeouts, disarming timer"); + return usbi_disarm_timer(&ctx->timer); +} +#else +static inline int arm_timer_for_next_timeout(struct libusb_context *ctx) +{ + UNUSED(ctx); + return 0; +} +#endif + +/* add a transfer to the (timeout-sorted) active transfers list. + * This function will return non 0 if fails to update the timer, + * in which case the transfer is *not* on the flying_transfers list. + * NB: flying_transfers_lock MUST be held when calling this. */ +static int add_to_flying_list(struct usbi_transfer *itransfer) +{ + struct usbi_transfer *cur; + struct timespec *timeout = &itransfer->timeout; + struct libusb_context *ctx = ITRANSFER_CTX(itransfer); + int r = 0; + int first = 1; + + calculate_timeout(itransfer); + + /* if we have no other flying transfers, start the list with this one */ + if (list_empty(&ctx->flying_transfers)) { + list_add(&itransfer->list, &ctx->flying_transfers); + goto out; + } + + /* if we have infinite timeout, append to end of list */ + if (!TIMESPEC_IS_SET(timeout)) { + list_add_tail(&itransfer->list, &ctx->flying_transfers); + /* first is irrelevant in this case */ + goto out; + } + + /* otherwise, find appropriate place in list */ + for_each_transfer(ctx, cur) { + /* find first timeout that occurs after the transfer in question */ + struct timespec *cur_ts = &cur->timeout; + + if (!TIMESPEC_IS_SET(cur_ts) || TIMESPEC_CMP(cur_ts, timeout, >)) { + list_add_tail(&itransfer->list, &cur->list); + goto out; + } + first = 0; + } + /* first is 0 at this stage (list not empty) */ + + /* otherwise we need to be inserted at the end */ + list_add_tail(&itransfer->list, &ctx->flying_transfers); +out: +#ifdef HAVE_OS_TIMER + if (first && usbi_using_timer(ctx) && TIMESPEC_IS_SET(timeout)) { + /* if this transfer has the lowest timeout of all active transfers, + * rearm the timer with this transfer's timeout */ +#ifdef ENABLE_LOGGING + struct libusb_transfer *transfer = USBI_TRANSFER_TO_LIBUSB_TRANSFER(itransfer); + usbi_dbg(ctx, "arm timer for timeout in %ums (first in line)", + transfer->timeout); +#endif + r = usbi_arm_timer(&ctx->timer, timeout); + } +#else + UNUSED(first); +#endif + + if (r) + list_del(&itransfer->list); + + return r; +} + +/* remove a transfer from the active transfers list. + * This function will *always* remove the transfer from the + * flying_transfers list. It will return a LIBUSB_ERROR code + * if it fails to update the timer for the next timeout. + * NB: flying_transfers_lock MUST be held when calling this. */ +static int remove_from_flying_list(struct usbi_transfer *itransfer) +{ + struct libusb_context *ctx = ITRANSFER_CTX(itransfer); + int rearm_timer; + int r = 0; + + rearm_timer = (TIMESPEC_IS_SET(&itransfer->timeout) && + list_first_entry(&ctx->flying_transfers, struct usbi_transfer, list) == itransfer); + list_del(&itransfer->list); + if (rearm_timer) + r = arm_timer_for_next_timeout(ctx); + + return r; +} + +/** \ingroup libusb_asyncio + * Submit a transfer. This function will fire off the USB transfer and then + * return immediately. + * + * \param transfer the transfer to submit + * \returns 0 on success + * \returns \ref LIBUSB_ERROR_NO_DEVICE if the device has been disconnected + * \returns \ref LIBUSB_ERROR_BUSY if the transfer has already been submitted. + * \returns \ref LIBUSB_ERROR_NOT_SUPPORTED if the transfer flags are not supported + * by the operating system. + * \returns \ref LIBUSB_ERROR_INVALID_PARAM if the transfer size is larger than + * the operating system and/or hardware can support (see \ref asynclimits) + * \returns another LIBUSB_ERROR code on other failure + */ +int API_EXPORTED libusb_submit_transfer(struct libusb_transfer *transfer) +{ + struct usbi_transfer *itransfer = + LIBUSB_TRANSFER_TO_USBI_TRANSFER(transfer); + struct libusb_context *ctx; + int r; + + assert(transfer->dev_handle); + if (itransfer->dev) + libusb_unref_device(itransfer->dev); + itransfer->dev = libusb_ref_device(transfer->dev_handle->dev); + + ctx = HANDLE_CTX(transfer->dev_handle); + usbi_dbg(ctx, "transfer %p", (void *) transfer); + + /* + * Important note on locking, this function takes / releases locks + * in the following order: + * take flying_transfers_lock + * take itransfer->lock + * clear transfer + * add to flying_transfers list + * release flying_transfers_lock + * submit transfer + * release itransfer->lock + * if submit failed: + * take flying_transfers_lock + * remove from flying_transfers list + * release flying_transfers_lock + * + * Note that it takes locks in the order a-b and then releases them + * in the same order a-b. This is somewhat unusual but not wrong, + * release order is not important as long as *all* locks are released + * before re-acquiring any locks. + * + * This means that the ordering of first releasing itransfer->lock + * and then re-acquiring the flying_transfers_list on error is + * important and must not be changed! + * + * This is done this way because when we take both locks we must always + * take flying_transfers_lock first to avoid ab-ba style deadlocks with + * the timeout handling and usbi_handle_disconnect paths. + * + * And we cannot release itransfer->lock before the submission is + * complete otherwise timeout handling for transfers with short + * timeouts may run before submission. + */ + usbi_mutex_lock(&ctx->flying_transfers_lock); + usbi_mutex_lock(&itransfer->lock); + if (itransfer->state_flags & USBI_TRANSFER_IN_FLIGHT) { + usbi_mutex_unlock(&ctx->flying_transfers_lock); + usbi_mutex_unlock(&itransfer->lock); + return LIBUSB_ERROR_BUSY; + } + itransfer->transferred = 0; + itransfer->state_flags = 0; + itransfer->timeout_flags = 0; + r = add_to_flying_list(itransfer); + if (r) { + usbi_mutex_unlock(&ctx->flying_transfers_lock); + usbi_mutex_unlock(&itransfer->lock); + return r; + } + /* + * We must release the flying transfers lock here, because with + * some backends the submit_transfer method is synchronous. + */ + usbi_mutex_unlock(&ctx->flying_transfers_lock); + + r = usbi_backend.submit_transfer(itransfer); + if (r == LIBUSB_SUCCESS) { + itransfer->state_flags |= USBI_TRANSFER_IN_FLIGHT; + } + usbi_mutex_unlock(&itransfer->lock); + + if (r != LIBUSB_SUCCESS) { + usbi_mutex_lock(&ctx->flying_transfers_lock); + remove_from_flying_list(itransfer); + usbi_mutex_unlock(&ctx->flying_transfers_lock); + } + + return r; +} + +/** \ingroup libusb_asyncio + * Asynchronously cancel a previously submitted transfer. + * This function returns immediately, but this does not indicate cancellation + * is complete. Your callback function will be invoked at some later time + * with a transfer status of + * \ref libusb_transfer_status::LIBUSB_TRANSFER_CANCELLED + * "LIBUSB_TRANSFER_CANCELLED." + * + * This function behaves differently on Darwin-based systems (macOS and iOS): + * + * - Calling this function for one transfer will cause all transfers on the + * same endpoint to be cancelled. Your callback function will be invoked with + * a transfer status of + * \ref libusb_transfer_status::LIBUSB_TRANSFER_CANCELLED + * "LIBUSB_TRANSFER_CANCELLED" for each transfer that was cancelled. + + * - When built for macOS versions prior to 10.5, this function sends a + * \c ClearFeature(ENDPOINT_HALT) request for the transfer's endpoint. + * (Prior to libusb 1.0.27, this request was sent on all Darwin systems.) + * If the device does not handle this request correctly, the data toggle + * bits for the endpoint can be left out of sync between host and device, + * which can have unpredictable results when the next data is sent on + * the endpoint, including data being silently lost. A call to + * \ref libusb_clear_halt will not resolve this situation, since that + * function uses the same request. Therefore, if your program runs on + * macOS < 10.5 (or libusb < 1.0.27), and uses a device that does not + * correctly implement \c ClearFeature(ENDPOINT_HALT) requests, it may + * only be safe to cancel transfers when followed by a device reset using + * \ref libusb_reset_device. + * + * \param transfer the transfer to cancel + * \returns 0 on success + * \returns \ref LIBUSB_ERROR_NOT_FOUND if the transfer is not in progress, + * already complete, or already cancelled. + * \returns a LIBUSB_ERROR code on failure + */ +int API_EXPORTED libusb_cancel_transfer(struct libusb_transfer *transfer) +{ + struct usbi_transfer *itransfer = + LIBUSB_TRANSFER_TO_USBI_TRANSFER(transfer); + struct libusb_context *ctx = ITRANSFER_CTX(itransfer); + int r; + + usbi_dbg(ctx, "transfer %p", (void *) transfer ); + usbi_mutex_lock(&itransfer->lock); + if (!(itransfer->state_flags & USBI_TRANSFER_IN_FLIGHT) + || (itransfer->state_flags & USBI_TRANSFER_CANCELLING)) { + r = LIBUSB_ERROR_NOT_FOUND; + goto out; + } + r = usbi_backend.cancel_transfer(itransfer); + if (r < 0) { + if (r != LIBUSB_ERROR_NOT_FOUND && + r != LIBUSB_ERROR_NO_DEVICE) + usbi_err(ctx, "cancel transfer failed error %d", r); + else + usbi_dbg(ctx, "cancel transfer failed error %d", r); + + if (r == LIBUSB_ERROR_NO_DEVICE) + itransfer->state_flags |= USBI_TRANSFER_DEVICE_DISAPPEARED; + } + + itransfer->state_flags |= USBI_TRANSFER_CANCELLING; + +out: + usbi_mutex_unlock(&itransfer->lock); + return r; +} + +/** \ingroup libusb_asyncio + * Set a transfers bulk stream id. Note users are advised to use + * libusb_fill_bulk_stream_transfer() instead of calling this function + * directly. + * + * Since version 1.0.19, \ref LIBUSB_API_VERSION >= 0x01000103 + * + * \param transfer the transfer to set the stream id for + * \param stream_id the stream id to set + * \see libusb_alloc_streams() + */ +void API_EXPORTED libusb_transfer_set_stream_id( + struct libusb_transfer *transfer, uint32_t stream_id) +{ + struct usbi_transfer *itransfer = + LIBUSB_TRANSFER_TO_USBI_TRANSFER(transfer); + + itransfer->stream_id = stream_id; +} + +/** \ingroup libusb_asyncio + * Get a transfers bulk stream id. + * + * Since version 1.0.19, \ref LIBUSB_API_VERSION >= 0x01000103 + * + * \param transfer the transfer to get the stream id for + * \returns the stream id for the transfer + */ +uint32_t API_EXPORTED libusb_transfer_get_stream_id( + struct libusb_transfer *transfer) +{ + struct usbi_transfer *itransfer = + LIBUSB_TRANSFER_TO_USBI_TRANSFER(transfer); + + return itransfer->stream_id; +} + +/* Handle completion of a transfer (completion might be an error condition). + * This will invoke the user-supplied callback function, which may end up + * freeing the transfer. Therefore you cannot use the transfer structure + * after calling this function, and you should free all backend-specific + * data before calling it. + * Do not call this function with the usbi_transfer lock held. User-specified + * callback functions may attempt to directly resubmit the transfer, which + * will attempt to take the lock. */ +int usbi_handle_transfer_completion(struct usbi_transfer *itransfer, + enum libusb_transfer_status status) +{ + struct libusb_transfer *transfer = + USBI_TRANSFER_TO_LIBUSB_TRANSFER(itransfer); + struct libusb_context *ctx = ITRANSFER_CTX(itransfer); + uint8_t flags; + int r; + + usbi_mutex_lock(&ctx->flying_transfers_lock); + r = remove_from_flying_list(itransfer); + usbi_mutex_unlock(&ctx->flying_transfers_lock); + if (r < 0) + usbi_err(ctx, "failed to set timer for next timeout"); + + usbi_mutex_lock(&itransfer->lock); + itransfer->state_flags &= ~USBI_TRANSFER_IN_FLIGHT; + usbi_mutex_unlock(&itransfer->lock); + + if (status == LIBUSB_TRANSFER_COMPLETED + && transfer->flags & LIBUSB_TRANSFER_SHORT_NOT_OK) { + int rqlen = transfer->length; + if (transfer->type == LIBUSB_TRANSFER_TYPE_CONTROL) + rqlen -= LIBUSB_CONTROL_SETUP_SIZE; + if (rqlen != itransfer->transferred) { + usbi_dbg(ctx, "interpreting short transfer as error"); + status = LIBUSB_TRANSFER_ERROR; + } + } + + flags = transfer->flags; + transfer->status = status; + transfer->actual_length = itransfer->transferred; + assert(transfer->actual_length >= 0); + usbi_dbg(ctx, "transfer %p has callback %p", + (void *) transfer, transfer->callback); + if (transfer->callback) { + libusb_lock_event_waiters (ctx); + transfer->callback(transfer); + libusb_unlock_event_waiters(ctx); + } + /* transfer might have been freed by the above call, do not use from + * this point. */ + if (flags & LIBUSB_TRANSFER_FREE_TRANSFER) + libusb_free_transfer(transfer); + return r; +} + +/* Similar to usbi_handle_transfer_completion() but exclusively for transfers + * that were asynchronously cancelled. The same concerns w.r.t. freeing of + * transfers exist here. + * Do not call this function with the usbi_transfer lock held. User-specified + * callback functions may attempt to directly resubmit the transfer, which + * will attempt to take the lock. */ +int usbi_handle_transfer_cancellation(struct usbi_transfer *itransfer) +{ + struct libusb_context *ctx = ITRANSFER_CTX(itransfer); + uint8_t timed_out; + + usbi_mutex_lock(&ctx->flying_transfers_lock); + timed_out = itransfer->timeout_flags & USBI_TRANSFER_TIMED_OUT; + usbi_mutex_unlock(&ctx->flying_transfers_lock); + + /* if the URB was cancelled due to timeout, report timeout to the user */ + if (timed_out) { + usbi_dbg(ctx, "detected timeout cancellation"); + return usbi_handle_transfer_completion(itransfer, LIBUSB_TRANSFER_TIMED_OUT); + } + + /* otherwise its a normal async cancel */ + return usbi_handle_transfer_completion(itransfer, LIBUSB_TRANSFER_CANCELLED); +} + +/* Add a completed transfer to the completed_transfers list of the + * context and signal the event. The backend's handle_transfer_completion() + * function will be called the next time an event handler runs. */ +void usbi_signal_transfer_completion(struct usbi_transfer *itransfer) +{ + struct libusb_device *dev = itransfer->dev; + + if (dev) { + struct libusb_context *ctx = DEVICE_CTX(dev); + unsigned int event_flags; + + usbi_mutex_lock(&ctx->event_data_lock); + event_flags = ctx->event_flags; + ctx->event_flags |= USBI_EVENT_TRANSFER_COMPLETED; + list_add_tail(&itransfer->completed_list, &ctx->completed_transfers); + if (!event_flags) + usbi_signal_event(&ctx->event); + usbi_mutex_unlock(&ctx->event_data_lock); + } +} + +/** \ingroup libusb_poll + * Attempt to acquire the event handling lock. This lock is used to ensure that + * only one thread is monitoring libusb event sources at any one time. + * + * You only need to use this lock if you are developing an application + * which calls poll() or select() on libusb's file descriptors directly. + * If you stick to libusb's event handling loop functions (e.g. + * libusb_handle_events()) then you do not need to be concerned with this + * locking. + * + * While holding this lock, you are trusted to actually be handling events. + * If you are no longer handling events, you must call libusb_unlock_events() + * as soon as possible. + * + * \param ctx the context to operate on, or NULL for the default context + * \returns 0 if the lock was obtained successfully + * \returns 1 if the lock was not obtained (i.e. another thread holds the lock) + * \ref libusb_mtasync + */ +int API_EXPORTED libusb_try_lock_events(libusb_context *ctx) +{ + int r; + unsigned int ru; + + ctx = usbi_get_context(ctx); + + /* is someone else waiting to close a device? if so, don't let this thread + * start event handling */ + usbi_mutex_lock(&ctx->event_data_lock); + ru = ctx->device_close; + usbi_mutex_unlock(&ctx->event_data_lock); + if (ru) { + usbi_dbg(ctx, "someone else is closing a device"); + return 1; + } + + r = usbi_mutex_trylock(&ctx->events_lock); + if (!r) + return 1; + + ctx->event_handler_active = 1; + return 0; +} + +/** \ingroup libusb_poll + * Acquire the event handling lock, blocking until successful acquisition if + * it is contended. This lock is used to ensure that only one thread is + * monitoring libusb event sources at any one time. + * + * You only need to use this lock if you are developing an application + * which calls poll() or select() on libusb's file descriptors directly. + * If you stick to libusb's event handling loop functions (e.g. + * libusb_handle_events()) then you do not need to be concerned with this + * locking. + * + * While holding this lock, you are trusted to actually be handling events. + * If you are no longer handling events, you must call libusb_unlock_events() + * as soon as possible. + * + * \param ctx the context to operate on, or NULL for the default context + * \ref libusb_mtasync + */ +void API_EXPORTED libusb_lock_events(libusb_context *ctx) +{ + ctx = usbi_get_context(ctx); + usbi_mutex_lock(&ctx->events_lock); + ctx->event_handler_active = 1; +} + +/** \ingroup libusb_poll + * Release the lock previously acquired with libusb_try_lock_events() or + * libusb_lock_events(). Releasing this lock will wake up any threads blocked + * on libusb_wait_for_event(). + * + * \param ctx the context to operate on, or NULL for the default context + * \ref libusb_mtasync + */ +void API_EXPORTED libusb_unlock_events(libusb_context *ctx) +{ + ctx = usbi_get_context(ctx); + ctx->event_handler_active = 0; + usbi_mutex_unlock(&ctx->events_lock); + + /* FIXME: perhaps we should be a bit more efficient by not broadcasting + * the availability of the events lock when we are modifying pollfds + * (check ctx->device_close)? */ + usbi_mutex_lock(&ctx->event_waiters_lock); + usbi_cond_broadcast(&ctx->event_waiters_cond); + usbi_mutex_unlock(&ctx->event_waiters_lock); +} + +/** \ingroup libusb_poll + * Determine if it is still OK for this thread to be doing event handling. + * + * Sometimes, libusb needs to temporarily pause all event handlers, and this + * is the function you should use before polling file descriptors to see if + * this is the case. + * + * If this function instructs your thread to give up the events lock, you + * should just continue the usual logic that is documented in \ref libusb_mtasync. + * On the next iteration, your thread will fail to obtain the events lock, + * and will hence become an event waiter. + * + * This function should be called while the events lock is held: you don't + * need to worry about the results of this function if your thread is not + * the current event handler. + * + * \param ctx the context to operate on, or NULL for the default context + * \returns 1 if event handling can start or continue + * \returns 0 if this thread must give up the events lock + * \ref fullstory "Multi-threaded I/O: the full story" + */ +int API_EXPORTED libusb_event_handling_ok(libusb_context *ctx) +{ + unsigned int r; + + ctx = usbi_get_context(ctx); + + /* is someone else waiting to close a device? if so, don't let this thread + * continue event handling */ + usbi_mutex_lock(&ctx->event_data_lock); + r = ctx->device_close; + usbi_mutex_unlock(&ctx->event_data_lock); + if (r) { + usbi_dbg(ctx, "someone else is closing a device"); + return 0; + } + + return 1; +} + + +/** \ingroup libusb_poll + * Determine if an active thread is handling events (i.e. if anyone is holding + * the event handling lock). + * + * \param ctx the context to operate on, or NULL for the default context + * \returns 1 if a thread is handling events + * \returns 0 if there are no threads currently handling events + * \ref libusb_mtasync + */ +int API_EXPORTED libusb_event_handler_active(libusb_context *ctx) +{ + unsigned int r; + + ctx = usbi_get_context(ctx); + + /* is someone else waiting to close a device? if so, don't let this thread + * start event handling -- indicate that event handling is happening */ + usbi_mutex_lock(&ctx->event_data_lock); + r = ctx->device_close; + usbi_mutex_unlock(&ctx->event_data_lock); + if (r) { + usbi_dbg(ctx, "someone else is closing a device"); + return 1; + } + + return ctx->event_handler_active; +} + +/** \ingroup libusb_poll + * Interrupt any active thread that is handling events. This is mainly useful + * for interrupting a dedicated event handling thread when an application + * wishes to call libusb_exit(). + * + * Since version 1.0.21, \ref LIBUSB_API_VERSION >= 0x01000105 + * + * \param ctx the context to operate on, or NULL for the default context + * \ref libusb_mtasync + */ +void API_EXPORTED libusb_interrupt_event_handler(libusb_context *ctx) +{ + unsigned int event_flags; + + usbi_dbg(ctx, " "); + + ctx = usbi_get_context(ctx); + usbi_mutex_lock(&ctx->event_data_lock); + + event_flags = ctx->event_flags; + ctx->event_flags |= USBI_EVENT_USER_INTERRUPT; + if (!event_flags) + usbi_signal_event(&ctx->event); + + usbi_mutex_unlock(&ctx->event_data_lock); +} + +/** \ingroup libusb_poll + * Acquire the event waiters lock. This lock is designed to be obtained under + * the situation where you want to be aware when events are completed, but + * some other thread is event handling so calling libusb_handle_events() is not + * allowed. + * + * You then obtain this lock, re-check that another thread is still handling + * events, then call libusb_wait_for_event(). + * + * You only need to use this lock if you are developing an application + * which calls poll() or select() on libusb's file descriptors directly, + * and may potentially be handling events from 2 threads simultaneously. + * If you stick to libusb's event handling loop functions (e.g. + * libusb_handle_events()) then you do not need to be concerned with this + * locking. + * + * \param ctx the context to operate on, or NULL for the default context + * \ref libusb_mtasync + */ +void API_EXPORTED libusb_lock_event_waiters(libusb_context *ctx) +{ + ctx = usbi_get_context(ctx); + usbi_mutex_lock(&ctx->event_waiters_lock); +} + +/** \ingroup libusb_poll + * Release the event waiters lock. + * \param ctx the context to operate on, or NULL for the default context + * \ref libusb_mtasync + */ +void API_EXPORTED libusb_unlock_event_waiters(libusb_context *ctx) +{ + ctx = usbi_get_context(ctx); + usbi_mutex_unlock(&ctx->event_waiters_lock); +} + +/** \ingroup libusb_poll + * Wait for another thread to signal completion of an event. Must be called + * with the event waiters lock held, see libusb_lock_event_waiters(). + * + * This function will block until any of the following conditions are met: + * -# The timeout expires + * -# A transfer completes + * -# A thread releases the event handling lock through libusb_unlock_events() + * + * Condition 1 is obvious. Condition 2 unblocks your thread after + * the callback for the transfer has completed. Condition 3 is important + * because it means that the thread that was previously handling events is no + * longer doing so, so if any events are to complete, another thread needs to + * step up and start event handling. + * + * This function releases the event waiters lock before putting your thread + * to sleep, and reacquires the lock as it is being woken up. + * + * \param ctx the context to operate on, or NULL for the default context + * \param tv maximum timeout for this blocking function. A NULL value + * indicates unlimited timeout. + * \returns 0 after a transfer completes or another thread stops event handling + * \returns 1 if the timeout expired + * \returns \ref LIBUSB_ERROR_INVALID_PARAM if timeval is invalid + * \ref libusb_mtasync + */ +int API_EXPORTED libusb_wait_for_event(libusb_context *ctx, struct timeval *tv) +{ + int r; + + ctx = usbi_get_context(ctx); + if (!tv) { + usbi_cond_wait(&ctx->event_waiters_cond, &ctx->event_waiters_lock); + return 0; + } + + if (!TIMEVAL_IS_VALID(tv)) + return LIBUSB_ERROR_INVALID_PARAM; + + r = usbi_cond_timedwait(&ctx->event_waiters_cond, + &ctx->event_waiters_lock, tv); + if (r < 0) + return r == LIBUSB_ERROR_TIMEOUT; + + return 0; +} + +/* NB: flying_transfers_lock must be held when calling this */ +static void handle_timeout(struct usbi_transfer *itransfer) +{ + struct libusb_transfer *transfer = + USBI_TRANSFER_TO_LIBUSB_TRANSFER(itransfer); + int r; + + itransfer->timeout_flags |= USBI_TRANSFER_TIMEOUT_HANDLED; + r = libusb_cancel_transfer(transfer); + if (r == LIBUSB_SUCCESS) + itransfer->timeout_flags |= USBI_TRANSFER_TIMED_OUT; + else + usbi_warn(TRANSFER_CTX(transfer), + "async cancel failed %d", r); +} + +/* NB: flying_transfers_lock must be held when calling this */ +static void handle_timeouts_locked(struct libusb_context *ctx) +{ + struct timespec systime; + struct usbi_transfer *itransfer; + + if (list_empty(&ctx->flying_transfers)) + return; + + /* get current time */ + usbi_get_monotonic_time(&systime); + + /* iterate through flying transfers list, finding all transfers that + * have expired timeouts */ + for_each_transfer(ctx, itransfer) { + struct timespec *cur_ts = &itransfer->timeout; + + /* if we've reached transfers of infinite timeout, we're all done */ + if (!TIMESPEC_IS_SET(cur_ts)) + return; + + /* ignore timeouts we've already handled */ + if (itransfer->timeout_flags & (USBI_TRANSFER_TIMEOUT_HANDLED | USBI_TRANSFER_OS_HANDLES_TIMEOUT)) + continue; + + /* if transfer has non-expired timeout, nothing more to do */ + if (TIMESPEC_CMP(cur_ts, &systime, >)) + return; + + /* otherwise, we've got an expired timeout to handle */ + handle_timeout(itransfer); + } +} + +static void handle_timeouts(struct libusb_context *ctx) +{ + ctx = usbi_get_context(ctx); + usbi_mutex_lock(&ctx->flying_transfers_lock); + handle_timeouts_locked(ctx); + usbi_mutex_unlock(&ctx->flying_transfers_lock); +} + +static int handle_event_trigger(struct libusb_context *ctx) +{ + struct list_head hotplug_msgs; + int hotplug_event = 0; + int r = 0; + + usbi_dbg(ctx, "event triggered"); + + list_init(&hotplug_msgs); + + /* take the the event data lock while processing events */ + usbi_mutex_lock(&ctx->event_data_lock); + + /* check if someone modified the event sources */ + if (ctx->event_flags & USBI_EVENT_EVENT_SOURCES_MODIFIED) + usbi_dbg(ctx, "someone updated the event sources"); + + if (ctx->event_flags & USBI_EVENT_USER_INTERRUPT) { + usbi_dbg(ctx, "someone purposefully interrupted"); + ctx->event_flags &= ~USBI_EVENT_USER_INTERRUPT; + } + + if (ctx->event_flags & USBI_EVENT_HOTPLUG_CB_DEREGISTERED) { + usbi_dbg(ctx, "someone unregistered a hotplug cb"); + ctx->event_flags &= ~USBI_EVENT_HOTPLUG_CB_DEREGISTERED; + hotplug_event = 1; + } + + /* check if someone is closing a device */ + if (ctx->event_flags & USBI_EVENT_DEVICE_CLOSE) + usbi_dbg(ctx, "someone is closing a device"); + + /* check for any pending hotplug messages */ + if (ctx->event_flags & USBI_EVENT_HOTPLUG_MSG_PENDING) { + usbi_dbg(ctx, "hotplug message received"); + ctx->event_flags &= ~USBI_EVENT_HOTPLUG_MSG_PENDING; + hotplug_event = 1; + assert(!list_empty(&ctx->hotplug_msgs)); + list_cut(&hotplug_msgs, &ctx->hotplug_msgs); + } + + /* complete any pending transfers */ + if (ctx->event_flags & USBI_EVENT_TRANSFER_COMPLETED) { + struct usbi_transfer *itransfer, *tmp; + struct list_head completed_transfers; + + assert(!list_empty(&ctx->completed_transfers)); + list_cut(&completed_transfers, &ctx->completed_transfers); + usbi_mutex_unlock(&ctx->event_data_lock); + + __for_each_completed_transfer_safe(&completed_transfers, itransfer, tmp) { + list_del(&itransfer->completed_list); + r = usbi_backend.handle_transfer_completion(itransfer); + if (r) { + usbi_err(ctx, "backend handle_transfer_completion failed with error %d", r); + break; + } + } + + usbi_mutex_lock(&ctx->event_data_lock); + if (!list_empty(&completed_transfers)) { + /* an error occurred, put the remaining transfers back on the list */ + list_splice_front(&completed_transfers, &ctx->completed_transfers); + } else if (list_empty(&ctx->completed_transfers)) { + ctx->event_flags &= ~USBI_EVENT_TRANSFER_COMPLETED; + } + } + + /* if no further pending events, clear the event */ + if (!ctx->event_flags) + usbi_clear_event(&ctx->event); + + usbi_mutex_unlock(&ctx->event_data_lock); + + /* process the hotplug events, if any */ + if (hotplug_event) + usbi_hotplug_process(ctx, &hotplug_msgs); + + return r; +} + +#ifdef HAVE_OS_TIMER +static int handle_timer_trigger(struct libusb_context *ctx) +{ + int r; + + usbi_mutex_lock(&ctx->flying_transfers_lock); + + /* process the timeout that just happened */ + handle_timeouts_locked(ctx); + + /* arm for next timeout */ + r = arm_timer_for_next_timeout(ctx); + + usbi_mutex_unlock(&ctx->flying_transfers_lock); + + return r; +} +#endif + +/* do the actual event handling. assumes that no other thread is concurrently + * doing the same thing. */ +static int handle_events(struct libusb_context *ctx, struct timeval *tv) +{ + struct usbi_reported_events reported_events; + int r, timeout_ms; + + /* prevent attempts to recursively handle events (e.g. calling into + * libusb_handle_events() from within a hotplug or transfer callback) */ + if (usbi_handling_events(ctx)) + return LIBUSB_ERROR_BUSY; + + /* only reallocate the event source data when the list of event sources has + * been modified since the last handle_events(), otherwise reuse them to + * save the additional overhead */ + usbi_mutex_lock(&ctx->event_data_lock); + if (ctx->event_flags & USBI_EVENT_EVENT_SOURCES_MODIFIED) { + usbi_dbg(ctx, "event sources modified, reallocating event data"); + + /* free anything removed since we last ran */ + cleanup_removed_event_sources(ctx); + + r = usbi_alloc_event_data(ctx); + if (r) { + usbi_mutex_unlock(&ctx->event_data_lock); + return r; + } + + /* reset the flag now that we have the updated list */ + ctx->event_flags &= ~USBI_EVENT_EVENT_SOURCES_MODIFIED; + + /* if no further pending events, clear the event so that we do + * not immediately return from the wait function */ + if (!ctx->event_flags) + usbi_clear_event(&ctx->event); + } + usbi_mutex_unlock(&ctx->event_data_lock); + + timeout_ms = (int)(tv->tv_sec * 1000) + (tv->tv_usec / 1000); + + /* round up to next millisecond */ + if (tv->tv_usec % 1000) + timeout_ms++; + + reported_events.event_bits = 0; + + usbi_start_event_handling(ctx); + + r = usbi_wait_for_events(ctx, &reported_events, timeout_ms); + if (r != LIBUSB_SUCCESS) { + if (r == LIBUSB_ERROR_TIMEOUT) { + handle_timeouts(ctx); + r = LIBUSB_SUCCESS; + } + goto done; + } + + if (reported_events.event_triggered) { + r = handle_event_trigger(ctx); + if (r) { + /* return error code */ + goto done; + } + } + +#ifdef HAVE_OS_TIMER + if (reported_events.timer_triggered) { + r = handle_timer_trigger(ctx); + if (r) { + /* return error code */ + goto done; + } + } +#endif + + if (!reported_events.num_ready) + goto done; + + r = usbi_backend.handle_events(ctx, reported_events.event_data, + reported_events.event_data_count, reported_events.num_ready); + if (r) + usbi_err(ctx, "backend handle_events failed with error %d", r); + +done: + usbi_end_event_handling(ctx); + return r; +} + +/* returns the smallest of: + * 1. timeout of next URB + * 2. user-supplied timeout + * returns 1 if there is an already-expired timeout, otherwise returns 0 + * and populates out + */ +static int get_next_timeout(libusb_context *ctx, struct timeval *tv, + struct timeval *out) +{ + struct timeval timeout; + int r = libusb_get_next_timeout(ctx, &timeout); + if (r) { + /* timeout already expired? */ + if (!timerisset(&timeout)) + return 1; + + /* choose the smallest of next URB timeout or user specified timeout */ + if (timercmp(&timeout, tv, <)) + *out = timeout; + else + *out = *tv; + } else { + *out = *tv; + } + return 0; +} + +/** \ingroup libusb_poll + * Handle any pending events. + * + * libusb determines "pending events" by checking if any timeouts have expired + * and by checking the set of file descriptors for activity. + * + * If a zero timeval is passed, this function will handle any already-pending + * events and then immediately return in non-blocking style. + * + * If a non-zero timeval is passed and no events are currently pending, this + * function will block waiting for events to handle up until the specified + * timeout. If an event arrives or a signal is raised, this function will + * return early. + * + * If the parameter completed is not NULL then after obtaining the event + * handling lock this function will return immediately if the integer + * pointed to is not 0. This allows for race free waiting for the completion + * of a specific transfer. + * + * \param ctx the context to operate on, or NULL for the default context + * \param tv the maximum time to block waiting for events, or an all zero + * timeval struct for non-blocking mode + * \param completed pointer to completion integer to check, or NULL + * \returns 0 on success + * \returns \ref LIBUSB_ERROR_INVALID_PARAM if timeval is invalid + * \returns another LIBUSB_ERROR code on other failure + * \ref libusb_mtasync + */ +int API_EXPORTED libusb_handle_events_timeout_completed(libusb_context *ctx, + struct timeval *tv, int *completed) +{ + int r; + struct timeval poll_timeout; + + if (!TIMEVAL_IS_VALID(tv)) + return LIBUSB_ERROR_INVALID_PARAM; + + ctx = usbi_get_context(ctx); + r = get_next_timeout(ctx, tv, &poll_timeout); + if (r) { + /* timeout already expired */ + handle_timeouts(ctx); + return 0; + } + +retry: + if (libusb_try_lock_events(ctx) == 0) { + if (completed == NULL || !*completed) { + /* we obtained the event lock: do our own event handling */ + usbi_dbg(ctx, "doing our own event handling"); + r = handle_events(ctx, &poll_timeout); + } + libusb_unlock_events(ctx); + return r; + } + + /* another thread is doing event handling. wait for thread events that + * notify event completion. */ + libusb_lock_event_waiters(ctx); + + if (completed && *completed) + goto already_done; + + if (!libusb_event_handler_active(ctx)) { + /* we hit a race: whoever was event handling earlier finished in the + * time it took us to reach this point. try the cycle again. */ + libusb_unlock_event_waiters(ctx); + usbi_dbg(ctx, "event handler was active but went away, retrying"); + goto retry; + } + + usbi_dbg(ctx, "another thread is doing event handling"); + r = libusb_wait_for_event(ctx, &poll_timeout); + +already_done: + libusb_unlock_event_waiters(ctx); + + if (r < 0) + return r; + else if (r == 1) + handle_timeouts(ctx); + return 0; +} + +/** \ingroup libusb_poll + * Handle any pending events + * + * Like libusb_handle_events_timeout_completed(), but without the completed + * parameter, calling this function is equivalent to calling + * libusb_handle_events_timeout_completed() with a NULL completed parameter. + * + * This function is kept primarily for backwards compatibility. + * All new code should call libusb_handle_events_completed() or + * libusb_handle_events_timeout_completed() to avoid race conditions. + * + * \param ctx the context to operate on, or NULL for the default context + * \param tv the maximum time to block waiting for events, or an all zero + * timeval struct for non-blocking mode + * \returns 0 on success, or a LIBUSB_ERROR code on failure + */ +int API_EXPORTED libusb_handle_events_timeout(libusb_context *ctx, + struct timeval *tv) +{ + return libusb_handle_events_timeout_completed(ctx, tv, NULL); +} + +/** \ingroup libusb_poll + * Handle any pending events in blocking mode. There is currently a timeout + * hard-coded at 60 seconds but we plan to make it unlimited in future. For + * finer control over whether this function is blocking or non-blocking, or + * for control over the timeout, use libusb_handle_events_timeout_completed() + * instead. + * + * This function is kept primarily for backwards compatibility. + * All new code should call libusb_handle_events_completed() or + * libusb_handle_events_timeout_completed() to avoid race conditions. + * + * \param ctx the context to operate on, or NULL for the default context + * \returns 0 on success, or a LIBUSB_ERROR code on failure + */ +int API_EXPORTED libusb_handle_events(libusb_context *ctx) +{ + struct timeval tv; + tv.tv_sec = 60; + tv.tv_usec = 0; + return libusb_handle_events_timeout_completed(ctx, &tv, NULL); +} + +/** \ingroup libusb_poll + * Handle any pending events in blocking mode. + * + * Like libusb_handle_events(), with the addition of a completed parameter + * to allow for race free waiting for the completion of a specific transfer. + * + * See libusb_handle_events_timeout_completed() for details on the completed + * parameter. + * + * \param ctx the context to operate on, or NULL for the default context + * \param completed pointer to completion integer to check, or NULL + * \returns 0 on success, or a LIBUSB_ERROR code on failure + * \ref libusb_mtasync + */ +int API_EXPORTED libusb_handle_events_completed(libusb_context *ctx, + int *completed) +{ + struct timeval tv; + tv.tv_sec = 60; + tv.tv_usec = 0; + return libusb_handle_events_timeout_completed(ctx, &tv, completed); +} + +/** \ingroup libusb_poll + * Handle any pending events by polling file descriptors, without checking if + * any other threads are already doing so. Must be called with the event lock + * held, see libusb_lock_events(). + * + * This function is designed to be called under the situation where you have + * taken the event lock and are calling poll()/select() directly on libusb's + * file descriptors (as opposed to using libusb_handle_events() or similar). + * You detect events on libusb's descriptors, so you then call this function + * with a zero timeout value (while still holding the event lock). + * + * \param ctx the context to operate on, or NULL for the default context + * \param tv the maximum time to block waiting for events, or zero for + * non-blocking mode + * \returns 0 on success + * \returns \ref LIBUSB_ERROR_INVALID_PARAM if timeval is invalid + * \returns another LIBUSB_ERROR code on other failure + * \ref libusb_mtasync + */ +int API_EXPORTED libusb_handle_events_locked(libusb_context *ctx, + struct timeval *tv) +{ + int r; + struct timeval poll_timeout; + + if (!TIMEVAL_IS_VALID(tv)) + return LIBUSB_ERROR_INVALID_PARAM; + + ctx = usbi_get_context(ctx); + r = get_next_timeout(ctx, tv, &poll_timeout); + if (r) { + /* timeout already expired */ + handle_timeouts(ctx); + return 0; + } + + return handle_events(ctx, &poll_timeout); +} + +/** \ingroup libusb_poll + * Determines whether your application must apply special timing considerations + * when monitoring libusb's file descriptors. + * + * This function is only useful for applications which retrieve and poll + * libusb's file descriptors in their own main loop (\ref libusb_pollmain). + * + * Ordinarily, libusb's event handler needs to be called into at specific + * moments in time (in addition to times when there is activity on the file + * descriptor set). The usual approach is to use libusb_get_next_timeout() + * to learn about when the next timeout occurs, and to adjust your + * poll()/select() timeout accordingly so that you can make a call into the + * library at that time. + * + * Some platforms supported by libusb do not come with this baggage - any + * events relevant to timing will be represented by activity on the file + * descriptor set, and libusb_get_next_timeout() will always return 0. + * This function allows you to detect whether you are running on such a + * platform. + * + * Since v1.0.5. + * + * \param ctx the context to operate on, or NULL for the default context + * \returns 0 if you must call into libusb at times determined by + * libusb_get_next_timeout(), or 1 if all timeout events are handled internally + * or through regular activity on the file descriptors. + * \ref libusb_pollmain "Polling libusb file descriptors for event handling" + */ +int API_EXPORTED libusb_pollfds_handle_timeouts(libusb_context *ctx) +{ + ctx = usbi_get_context(ctx); + return usbi_using_timer(ctx); +} + +/** \ingroup libusb_poll + * Determine the next internal timeout that libusb needs to handle. You only + * need to use this function if you are calling poll() or select() or similar + * on libusb's file descriptors yourself - you do not need to use it if you + * are calling libusb_handle_events() or a variant directly. + * + * You should call this function in your main loop in order to determine how + * long to wait for select() or poll() to return results. libusb needs to be + * called into at this timeout, so you should use it as an upper bound on + * your select() or poll() call. + * + * When the timeout has expired, call into libusb_handle_events_timeout() + * (perhaps in non-blocking mode) so that libusb can handle the timeout. + * + * This function may return 1 (success) and an all-zero timeval. If this is + * the case, it indicates that libusb has a timeout that has already expired + * so you should call libusb_handle_events_timeout() or similar immediately. + * A return code of 0 indicates that there are no pending timeouts. + * + * On some platforms, this function will always returns 0 (no pending + * timeouts). See \ref polltime. + * + * \param ctx the context to operate on, or NULL for the default context + * \param tv output location for a relative time against the current + * clock in which libusb must be called into in order to process timeout events + * \returns 0 if there are no pending timeouts, 1 if a timeout was returned, + * or \ref LIBUSB_ERROR_OTHER on failure + */ +int API_EXPORTED libusb_get_next_timeout(libusb_context *ctx, + struct timeval *tv) +{ + struct usbi_transfer *itransfer; + struct timespec systime; + struct timespec next_timeout = { 0, 0 }; + + ctx = usbi_get_context(ctx); + if (usbi_using_timer(ctx)) + return 0; + + usbi_mutex_lock(&ctx->flying_transfers_lock); + if (list_empty(&ctx->flying_transfers)) { + usbi_mutex_unlock(&ctx->flying_transfers_lock); + usbi_dbg(ctx, "no URBs, no timeout!"); + return 0; + } + + /* find next transfer which hasn't already been processed as timed out */ + for_each_transfer(ctx, itransfer) { + if (itransfer->timeout_flags & (USBI_TRANSFER_TIMEOUT_HANDLED | USBI_TRANSFER_OS_HANDLES_TIMEOUT)) + continue; + + /* if we've reached transfers of infinite timeout, we're done looking */ + if (!TIMESPEC_IS_SET(&itransfer->timeout)) + break; + + next_timeout = itransfer->timeout; + break; + } + usbi_mutex_unlock(&ctx->flying_transfers_lock); + + if (!TIMESPEC_IS_SET(&next_timeout)) { + usbi_dbg(ctx, "no URB with timeout or all handled by OS; no timeout!"); + return 0; + } + + usbi_get_monotonic_time(&systime); + + if (!TIMESPEC_CMP(&systime, &next_timeout, <)) { + usbi_dbg(ctx, "first timeout already expired"); + timerclear(tv); + } else { + TIMESPEC_SUB(&next_timeout, &systime, &next_timeout); + TIMESPEC_TO_TIMEVAL(tv, &next_timeout); + usbi_dbg(ctx, "next timeout in %ld.%06lds", (long)tv->tv_sec, (long)tv->tv_usec); + } + + return 1; +} + +/** \ingroup libusb_poll + * Register notification functions for file descriptor additions/removals. + * These functions will be invoked for every new or removed file descriptor + * that libusb uses as an event source. + * + * To remove notifiers, pass NULL values for the function pointers. + * + * Note that file descriptors may have been added even before you register + * these notifiers (e.g. at libusb_init_context() time). + * + * Additionally, note that the removal notifier may be called during + * libusb_exit() (e.g. when it is closing file descriptors that were opened + * and added to the poll set at libusb_init_context() time). If you don't want this, + * remove the notifiers immediately before calling libusb_exit(). + * + * \param ctx the context to operate on, or NULL for the default context + * \param added_cb pointer to function for addition notifications + * \param removed_cb pointer to function for removal notifications + * \param user_data User data to be passed back to callbacks (useful for + * passing context information) + */ +void API_EXPORTED libusb_set_pollfd_notifiers(libusb_context *ctx, + libusb_pollfd_added_cb added_cb, libusb_pollfd_removed_cb removed_cb, + void *user_data) +{ +#if !defined(PLATFORM_WINDOWS) + ctx = usbi_get_context(ctx); + ctx->fd_added_cb = added_cb; + ctx->fd_removed_cb = removed_cb; + ctx->fd_cb_user_data = user_data; +#else + usbi_err(ctx, "external polling of libusb's internal event sources " \ + "is not yet supported on Windows"); + UNUSED(added_cb); + UNUSED(removed_cb); + UNUSED(user_data); +#endif +} + +/* + * Interrupt the iteration of the event handling thread, so that it picks + * up the event source change. Callers of this function must hold the event_data_lock. + */ +static void usbi_event_source_notification(struct libusb_context *ctx) +{ + unsigned int event_flags; + + /* Record that there is a new poll fd. + * Only signal an event if there are no prior pending events. */ + event_flags = ctx->event_flags; + ctx->event_flags |= USBI_EVENT_EVENT_SOURCES_MODIFIED; + if (!event_flags) + usbi_signal_event(&ctx->event); +} + +/* Add an event source to the list of event sources to be monitored. + * poll_events should be specified as a bitmask of events passed to poll(), e.g. + * POLLIN and/or POLLOUT. */ +int usbi_add_event_source(struct libusb_context *ctx, usbi_os_handle_t os_handle, short poll_events) +{ + struct usbi_event_source *ievent_source = malloc(sizeof(*ievent_source)); + + if (!ievent_source) + return LIBUSB_ERROR_NO_MEM; + + usbi_dbg(ctx, "add " USBI_OS_HANDLE_FORMAT_STRING " events %d", os_handle, poll_events); + ievent_source->data.os_handle = os_handle; + ievent_source->data.poll_events = poll_events; + usbi_mutex_lock(&ctx->event_data_lock); + list_add_tail(&ievent_source->list, &ctx->event_sources); + usbi_event_source_notification(ctx); + usbi_mutex_unlock(&ctx->event_data_lock); + +#if !defined(PLATFORM_WINDOWS) + if (ctx->fd_added_cb) + ctx->fd_added_cb(os_handle, poll_events, ctx->fd_cb_user_data); +#endif + + return 0; +} + +/* Remove an event source from the list of event sources to be monitored. */ +void usbi_remove_event_source(struct libusb_context *ctx, usbi_os_handle_t os_handle) +{ + struct usbi_event_source *ievent_source; + int found = 0; + + usbi_dbg(ctx, "remove " USBI_OS_HANDLE_FORMAT_STRING, os_handle); + usbi_mutex_lock(&ctx->event_data_lock); + for_each_event_source(ctx, ievent_source) { + if (ievent_source->data.os_handle == os_handle) { + found = 1; + break; + } + } + + if (!found) { + usbi_dbg(ctx, "couldn't find " USBI_OS_HANDLE_FORMAT_STRING " to remove", os_handle); + usbi_mutex_unlock(&ctx->event_data_lock); + return; + } + + list_del(&ievent_source->list); + list_add_tail(&ievent_source->list, &ctx->removed_event_sources); + usbi_event_source_notification(ctx); + usbi_mutex_unlock(&ctx->event_data_lock); + +#if !defined(PLATFORM_WINDOWS) + if (ctx->fd_removed_cb) + ctx->fd_removed_cb(os_handle, ctx->fd_cb_user_data); +#endif +} + +/** \ingroup libusb_poll + * Retrieve a list of file descriptors that should be polled by your main loop + * as libusb event sources. + * + * The returned list is NULL-terminated and should be freed with libusb_free_pollfds() + * when done. The actual list contents must not be touched. + * + * As file descriptors are a Unix-specific concept, this function is not + * available on Windows and will always return NULL. + * + * \param ctx the context to operate on, or NULL for the default context + * \returns a NULL-terminated list of libusb_pollfd structures + * \returns NULL on error + * \returns NULL on platforms where the functionality is not available + */ +DEFAULT_VISIBILITY +const struct libusb_pollfd ** LIBUSB_CALL libusb_get_pollfds( + libusb_context *ctx) +{ +#if !defined(PLATFORM_WINDOWS) + struct libusb_pollfd **ret = NULL; + struct usbi_event_source *ievent_source; + size_t i; + + static_assert(sizeof(struct usbi_event_source_data) == sizeof(struct libusb_pollfd), + "mismatch between usbi_event_source_data and libusb_pollfd sizes"); + + ctx = usbi_get_context(ctx); + + usbi_mutex_lock(&ctx->event_data_lock); + + i = 0; + for_each_event_source(ctx, ievent_source) + i++; + + ret = calloc(i + 1, sizeof(struct libusb_pollfd *)); + if (!ret) + goto out; + + i = 0; + for_each_event_source(ctx, ievent_source) + ret[i++] = (struct libusb_pollfd *)ievent_source; + +out: + usbi_mutex_unlock(&ctx->event_data_lock); + return (const struct libusb_pollfd **)ret; +#else + usbi_err(ctx, "external polling of libusb's internal event sources " \ + "is not yet supported on Windows"); + return NULL; +#endif +} + +/** \ingroup libusb_poll + * Free a list of libusb_pollfd structures. This should be called for all + * pollfd lists allocated with libusb_get_pollfds(). + * + * Since version 1.0.20, \ref LIBUSB_API_VERSION >= 0x01000104 + * + * It is legal to call this function with a NULL pollfd list. In this case, + * the function will simply do nothing. + * + * \param pollfds the list of libusb_pollfd structures to free + */ +void API_EXPORTED libusb_free_pollfds(const struct libusb_pollfd **pollfds) +{ +#if !defined(PLATFORM_WINDOWS) + free((void *)pollfds); +#else + UNUSED(pollfds); +#endif +} + +/* Backends may call this from handle_events to report disconnection of a + * device. This function ensures transfers get cancelled appropriately. + * Callers of this function must hold the events_lock. + */ +void usbi_handle_disconnect(struct libusb_context *ctx, struct libusb_device_handle *dev_handle) +{ + struct usbi_transfer *cur; + struct usbi_transfer *to_cancel; + + usbi_dbg(ctx, "device %d.%d", + dev_handle->dev->bus_number, dev_handle->dev->device_address); + + /* terminate all pending transfers with the LIBUSB_TRANSFER_NO_DEVICE + * status code. + * + * when we find a transfer for this device on the list, there are two + * possible scenarios: + * 1. the transfer is currently in-flight, in which case we terminate the + * transfer here + * 2. the transfer has been added to the flying transfer list by + * libusb_submit_transfer, has failed to submit and + * libusb_submit_transfer is waiting for us to release the + * flying_transfers_lock to remove it, so we ignore it + */ + + while (1) { + to_cancel = NULL; + usbi_mutex_lock(&ctx->flying_transfers_lock); + for_each_transfer(ctx, cur) { + struct libusb_transfer *cur_transfer = USBI_TRANSFER_TO_LIBUSB_TRANSFER(cur); + if (cur_transfer->dev_handle == dev_handle) { + usbi_mutex_lock(&cur->lock); + if (cur->state_flags & USBI_TRANSFER_IN_FLIGHT) + to_cancel = cur; + usbi_mutex_unlock(&cur->lock); + + if (to_cancel) + break; + } + } + usbi_mutex_unlock(&ctx->flying_transfers_lock); + + if (!to_cancel) + break; + +#ifdef ENABLE_LOGGING + struct libusb_transfer *transfer_to_cancel = USBI_TRANSFER_TO_LIBUSB_TRANSFER(to_cancel); + usbi_dbg(ctx, "cancelling transfer %p from disconnect", + (void *) transfer_to_cancel); +#endif + + usbi_mutex_lock(&to_cancel->lock); + usbi_backend.clear_transfer_priv(to_cancel); + usbi_mutex_unlock(&to_cancel->lock); + usbi_handle_transfer_completion(to_cancel, LIBUSB_TRANSFER_NO_DEVICE); + } +} diff --git a/usbaudio/src/main/cpp/external/libusb/libusb/libusb.h b/usbaudio/src/main/cpp/external/libusb/libusb/libusb.h new file mode 100644 index 000000000..c641955f8 --- /dev/null +++ b/usbaudio/src/main/cpp/external/libusb/libusb/libusb.h @@ -0,0 +1,2471 @@ +/* + * Public libusb header file + * Copyright © 2001 Johannes Erdfelt + * Copyright © 2007-2008 Daniel Drake + * Copyright © 2012 Pete Batard + * Copyright © 2012-2023 Nathan Hjelm + * Copyright © 2014-2020 Chris Dickens + * For more information, please visit: https://libusb.info + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef LIBUSB_H +#define LIBUSB_H + +#if defined(_MSC_VER) +#pragma warning(push) +/* Disable: warning C4200: nonstandard extension used : zero-sized array in struct/union */ +#pragma warning(disable:4200) +/* on MS environments, the inline keyword is available in C++ only */ +#if !defined(__cplusplus) +#define inline __inline +#endif +/* ssize_t is also not available */ +#ifndef _SSIZE_T_DEFINED +#define _SSIZE_T_DEFINED +#include +typedef SSIZE_T ssize_t; +#endif /* _SSIZE_T_DEFINED */ +#endif /* _MSC_VER */ + +#include +#include +#include +#if !defined(_MSC_VER) +#include +#endif +#include + +#if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) +#define LIBUSB_FLEXIBLE_ARRAY /* [] - valid C99 code */ +#else +#define LIBUSB_FLEXIBLE_ARRAY 0 /* [0] - non-standard, but usually working code */ +#endif /* __STDC_VERSION__ */ + +/* 'interface' might be defined as a macro on Windows, so we need to + * undefine it so as not to break the current libusb API, because + * libusb_config_descriptor has an 'interface' member + * As this can be problematic if you include windows.h after libusb.h + * in your sources, we force windows.h to be included first. */ +#if defined(_WIN32) || defined(__CYGWIN__) +#include +#if defined(interface) +#undef interface +#endif +#endif /* _WIN32 || __CYGWIN__ */ + +#if defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 5)) +#define LIBUSB_DEPRECATED_FOR(f) __attribute__ ((deprecated ("Use " #f " instead"))) +#elif defined(__GNUC__) && (__GNUC__ >= 3) +#define LIBUSB_DEPRECATED_FOR(f) __attribute__ ((deprecated)) +#elif defined(_MSC_VER) +#define LIBUSB_DEPRECATED_FOR(f) __declspec(deprecated("Use " #f " instead")) +#else +#define LIBUSB_DEPRECATED_FOR(f) +#endif /* __GNUC__ */ + +#if defined(__GNUC__) +#define LIBUSB_PACKED __attribute__ ((packed)) +#else +#define LIBUSB_PACKED +#endif /* __GNUC__ */ + +/** \def LIBUSB_CALL + * \ingroup libusb_misc + * libusb's Windows calling convention. + * + * Under Windows, the selection of available compilers and configurations + * means that, unlike other platforms, there is not one true calling + * convention (calling convention: the manner in which parameters are + * passed to functions in the generated assembly code). + * + * Matching the Windows API itself, libusb uses the WINAPI convention (which + * translates to the stdcall convention) and guarantees that the + * library is compiled in this way. The public header file also includes + * appropriate annotations so that your own software will use the right + * convention, even if another convention is being used by default within + * your codebase. + * + * The one consideration that you must apply in your software is to mark + * all functions which you use as libusb callbacks with this LIBUSB_CALL + * annotation, so that they too get compiled for the correct calling + * convention. + * + * On non-Windows operating systems, this macro is defined as nothing. This + * means that you can apply it to your code without worrying about + * cross-platform compatibility. + */ +/* LIBUSB_CALL must be defined on both definition and declaration of libusb + * functions. You'd think that declaration would be enough, but cygwin will + * complain about conflicting types unless both are marked this way. + * The placement of this macro is important too; it must appear after the + * return type, before the function name. See internal documentation for + * API_EXPORTED. + */ +#if defined(_WIN32) || defined(__CYGWIN__) +#define LIBUSB_CALL WINAPI +#define LIBUSB_CALLV WINAPIV +#else +#define LIBUSB_CALL +#define LIBUSB_CALLV +#endif /* _WIN32 || __CYGWIN__ */ + +/** \def LIBUSB_API_VERSION + * \ingroup libusb_misc + * libusb's API version. + * + * Since version 1.0.18, to help with feature detection, libusb defines + * a LIBUSB_API_VERSION macro that gets increased every time there is a + * significant change to the API, such as the introduction of a new call, + * the definition of a new macro/enum member, or any other element that + * libusb applications may want to detect at compilation time. + * + * Between versions 1.0.13 and 1.0.17 (inclusive) the older spelling of + * LIBUSBX_API_VERSION was used. + * + * The macro is typically used in an application as follows: + * \code + * #if defined(LIBUSB_API_VERSION) && (LIBUSB_API_VERSION >= 0x01001234) + * // Use one of the newer features from the libusb API + * #endif + * \endcode + * + * Internally, LIBUSB_API_VERSION is defined as follows: + * (libusb major << 24) | (libusb minor << 16) | (16 bit incremental) + * + * The incremental component has changed as follows: + *
    + *
  • libusbx version 1.0.13: LIBUSBX_API_VERSION = 0x01000100 + *
  • libusbx version 1.0.14: LIBUSBX_API_VERSION = 0x010000FF + *
  • libusbx version 1.0.15: LIBUSBX_API_VERSION = 0x01000101 + *
  • libusbx version 1.0.16: LIBUSBX_API_VERSION = 0x01000102 + *
  • libusbx version 1.0.17: LIBUSBX_API_VERSION = 0x01000102 + *
  • libusb version 1.0.18: LIBUSB_API_VERSION = 0x01000102 + *
  • libusb version 1.0.19: LIBUSB_API_VERSION = 0x01000103 + *
  • libusb version 1.0.20: LIBUSB_API_VERSION = 0x01000104 + *
  • libusb version 1.0.21: LIBUSB_API_VERSION = 0x01000105 + *
  • libusb version 1.0.22: LIBUSB_API_VERSION = 0x01000106 + *
  • libusb version 1.0.23: LIBUSB_API_VERSION = 0x01000107 + *
  • libusb version 1.0.24: LIBUSB_API_VERSION = 0x01000108 + *
  • libusb version 1.0.25: LIBUSB_API_VERSION = 0x01000109 + *
  • libusb version 1.0.26: LIBUSB_API_VERSION = 0x01000109 + *
  • libusb version 1.0.27: LIBUSB_API_VERSION = 0x0100010A + *
  • libusb version 1.0.28: LIBUSB_API_VERSION = 0x0100010A + *
  • libusb version 1.0.29: LIBUSB_API_VERSION = 0x0100010B + *
  • libusb version 1.0.30: LIBUSB_API_VERSION = 0x0100010C + *
+ */ +#define LIBUSB_API_VERSION 0x0100010C + +/** \def LIBUSBX_API_VERSION + * \ingroup libusb_misc + * + * This is the older spelling, kept for backwards compatibility of code + * needing to test for older library versions where the newer spelling + * did not exist. */ +#define LIBUSBX_API_VERSION LIBUSB_API_VERSION + +#if defined(__cplusplus) +extern "C" { +#endif + +/** + * \ingroup libusb_misc + * Convert a 16-bit value from host-endian to little-endian format. On + * little endian systems, this function does nothing. On big endian systems, + * the bytes are swapped. + * \param x the host-endian value to convert + * \returns the value in little-endian byte order + */ +static inline uint16_t libusb_cpu_to_le16(uint16_t x) +{ + union { + uint8_t b8[2]; + uint16_t b16; + } _tmp; + _tmp.b8[1] = (uint8_t) (x >> 8); + _tmp.b8[0] = (uint8_t) (x & 0xff); + return _tmp.b16; +} + +/** \def libusb_le16_to_cpu + * \ingroup libusb_misc + * Convert a 16-bit value from little-endian to host-endian format. On + * little endian systems, this function does nothing. On big endian systems, + * the bytes are swapped. + * \param x the little-endian value to convert + * \returns the value in host-endian byte order + */ +#define libusb_le16_to_cpu libusb_cpu_to_le16 + +/* standard USB stuff */ + +/** \ingroup libusb_desc + * Device and/or Interface Class codes */ +enum libusb_class_code { + /** In the context of a \ref libusb_device_descriptor "device descriptor", + * this bDeviceClass value indicates that each interface specifies its + * own class information and all interfaces operate independently. + */ + LIBUSB_CLASS_PER_INTERFACE = 0x00, + + /** Audio class */ + LIBUSB_CLASS_AUDIO = 0x01, + + /** Communications class */ + LIBUSB_CLASS_COMM = 0x02, + + /** Human Interface Device class */ + LIBUSB_CLASS_HID = 0x03, + + /** Physical */ + LIBUSB_CLASS_PHYSICAL = 0x05, + + /** Image class */ + LIBUSB_CLASS_IMAGE = 0x06, + LIBUSB_CLASS_PTP = 0x06, /* legacy name from libusb-0.1 usb.h */ + + /** Printer class */ + LIBUSB_CLASS_PRINTER = 0x07, + + /** Mass storage class */ + LIBUSB_CLASS_MASS_STORAGE = 0x08, + + /** Hub class */ + LIBUSB_CLASS_HUB = 0x09, + + /** Data class */ + LIBUSB_CLASS_DATA = 0x0a, + + /** Smart Card */ + LIBUSB_CLASS_SMART_CARD = 0x0b, + + /** Content Security */ + LIBUSB_CLASS_CONTENT_SECURITY = 0x0d, + + /** Video */ + LIBUSB_CLASS_VIDEO = 0x0e, + + /** Personal Healthcare */ + LIBUSB_CLASS_PERSONAL_HEALTHCARE = 0x0f, + + /** Audio & Video */ + LIBUSB_CLASS_AUDIO_VIDEO = 0x10, + + /** Billboard */ + LIBUSB_CLASS_BILLBOARD = 0x11, + + /** Interface class */ + LIBUSB_CLASS_TYPE_C_BRIDGE = 0x12, + + /** Bulk display */ + LIBUSB_CLASS_BULK_DISPLAY_PROTOCOL = 0x13, + + /** MCTP */ + LIBUSB_CLASS_MCTP = 0x14, + + /** I3C */ + LIBUSB_CLASS_I3C = 0x3c, + + /** Diagnostic Device */ + LIBUSB_CLASS_DIAGNOSTIC_DEVICE = 0xdc, + + /** Wireless class */ + LIBUSB_CLASS_WIRELESS = 0xe0, + + /** Miscellaneous class */ + LIBUSB_CLASS_MISCELLANEOUS = 0xef, + + /** Application class */ + LIBUSB_CLASS_APPLICATION = 0xfe, + + /** Class is vendor-specific */ + LIBUSB_CLASS_VENDOR_SPEC = 0xff +}; + +/** \ingroup libusb_desc + * Descriptor types as defined by the USB specification. */ +enum libusb_descriptor_type { + /** Device descriptor. See libusb_device_descriptor. */ + LIBUSB_DT_DEVICE = 0x01, + + /** Configuration descriptor. See libusb_config_descriptor. */ + LIBUSB_DT_CONFIG = 0x02, + + /** String descriptor */ + LIBUSB_DT_STRING = 0x03, + + /** Interface descriptor. See libusb_interface_descriptor. */ + LIBUSB_DT_INTERFACE = 0x04, + + /** Endpoint descriptor. See libusb_endpoint_descriptor. */ + LIBUSB_DT_ENDPOINT = 0x05, + + /** Interface Association Descriptor. + * See libusb_interface_association_descriptor */ + LIBUSB_DT_INTERFACE_ASSOCIATION = 0x0b, + + /** BOS descriptor */ + LIBUSB_DT_BOS = 0x0f, + + /** Device Capability descriptor */ + LIBUSB_DT_DEVICE_CAPABILITY = 0x10, + + /** HID descriptor */ + LIBUSB_DT_HID = 0x21, + + /** HID report descriptor */ + LIBUSB_DT_REPORT = 0x22, + + /** Physical descriptor */ + LIBUSB_DT_PHYSICAL = 0x23, + + /** Hub descriptor */ + LIBUSB_DT_HUB = 0x29, + + /** SuperSpeed Hub descriptor */ + LIBUSB_DT_SUPERSPEED_HUB = 0x2a, + + /** SuperSpeed Endpoint Companion descriptor */ + LIBUSB_DT_SS_ENDPOINT_COMPANION = 0x30 +}; + +/* Descriptor sizes per descriptor type */ +#define LIBUSB_DT_DEVICE_SIZE 18 +#define LIBUSB_DT_CONFIG_SIZE 9 +#define LIBUSB_DT_INTERFACE_SIZE 9 +#define LIBUSB_DT_ENDPOINT_SIZE 7 +#define LIBUSB_DT_ENDPOINT_AUDIO_SIZE 9 /* Audio extension */ +#define LIBUSB_DT_HUB_NONVAR_SIZE 7 +#define LIBUSB_DT_SS_ENDPOINT_COMPANION_SIZE 6 +#define LIBUSB_DT_BOS_SIZE 5 +#define LIBUSB_DT_DEVICE_CAPABILITY_SIZE 3 +#define LIBUSB_DT_INTERFACE_ASSOCIATION_SIZE 8 + +/* BOS descriptor sizes */ +#define LIBUSB_BT_USB_2_0_EXTENSION_SIZE 7 +#define LIBUSB_BT_SS_USB_DEVICE_CAPABILITY_SIZE 10 +#define LIBUSB_BT_SSPLUS_USB_DEVICE_CAPABILITY_SIZE 12 +#define LIBUSB_BT_CONTAINER_ID_SIZE 20 +#define LIBUSB_BT_PLATFORM_DESCRIPTOR_MIN_SIZE 20 + +/* We unwrap the BOS => define its max size */ +#define LIBUSB_DT_BOS_MAX_SIZE \ + (LIBUSB_DT_BOS_SIZE + \ + LIBUSB_BT_USB_2_0_EXTENSION_SIZE + \ + LIBUSB_BT_SS_USB_DEVICE_CAPABILITY_SIZE + \ + LIBUSB_BT_CONTAINER_ID_SIZE) + +#define LIBUSB_ENDPOINT_ADDRESS_MASK 0x0f /* in bEndpointAddress */ +#define LIBUSB_ENDPOINT_DIR_MASK 0x80 + +/** \ingroup libusb_desc + * Endpoint direction. Values for bit 7 of the + * \ref libusb_endpoint_descriptor::bEndpointAddress "endpoint address" scheme. + */ +enum libusb_endpoint_direction { + /** Out: host-to-device */ + LIBUSB_ENDPOINT_OUT = 0x00, + + /** In: device-to-host */ + LIBUSB_ENDPOINT_IN = 0x80 +}; + +#define LIBUSB_TRANSFER_TYPE_MASK 0x03 /* in bmAttributes */ + +/** \ingroup libusb_desc + * Endpoint transfer type. Values for bits 0:1 of the + * \ref libusb_endpoint_descriptor::bmAttributes "endpoint attributes" field. + */ +enum libusb_endpoint_transfer_type { + /** Control endpoint */ + LIBUSB_ENDPOINT_TRANSFER_TYPE_CONTROL = 0x0, + + /** Isochronous endpoint */ + LIBUSB_ENDPOINT_TRANSFER_TYPE_ISOCHRONOUS = 0x1, + + /** Bulk endpoint */ + LIBUSB_ENDPOINT_TRANSFER_TYPE_BULK = 0x2, + + /** Interrupt endpoint */ + LIBUSB_ENDPOINT_TRANSFER_TYPE_INTERRUPT = 0x3 +}; + +/** \ingroup libusb_misc + * Standard requests, as defined in table 9-5 of the USB 3.0 specifications */ +enum libusb_standard_request { + /** Request status of the specific recipient */ + LIBUSB_REQUEST_GET_STATUS = 0x00, + + /** Clear or disable a specific feature */ + LIBUSB_REQUEST_CLEAR_FEATURE = 0x01, + + /* 0x02 is reserved */ + + /** Set or enable a specific feature */ + LIBUSB_REQUEST_SET_FEATURE = 0x03, + + /* 0x04 is reserved */ + + /** Set device address for all future accesses */ + LIBUSB_REQUEST_SET_ADDRESS = 0x05, + + /** Get the specified descriptor */ + LIBUSB_REQUEST_GET_DESCRIPTOR = 0x06, + + /** Used to update existing descriptors or add new descriptors */ + LIBUSB_REQUEST_SET_DESCRIPTOR = 0x07, + + /** Get the current device configuration value */ + LIBUSB_REQUEST_GET_CONFIGURATION = 0x08, + + /** Set device configuration */ + LIBUSB_REQUEST_SET_CONFIGURATION = 0x09, + + /** Return the selected alternate setting for the specified interface */ + LIBUSB_REQUEST_GET_INTERFACE = 0x0a, + + /** Select an alternate interface for the specified interface */ + LIBUSB_REQUEST_SET_INTERFACE = 0x0b, + + /** Set then report an endpoint's synchronization frame */ + LIBUSB_REQUEST_SYNCH_FRAME = 0x0c, + + /** Sets both the U1 and U2 Exit Latency */ + LIBUSB_REQUEST_SET_SEL = 0x30, + + /** Delay from the time a host transmits a packet to the time it is + * received by the device. */ + LIBUSB_SET_ISOCH_DELAY = 0x31 +}; + +/** \ingroup libusb_misc + * Request type bits of the + * \ref libusb_control_setup::bmRequestType "bmRequestType" field in control + * transfers. */ +enum libusb_request_type { + /** Standard */ + LIBUSB_REQUEST_TYPE_STANDARD = (0x00 << 5), + + /** Class */ + LIBUSB_REQUEST_TYPE_CLASS = (0x01 << 5), + + /** Vendor */ + LIBUSB_REQUEST_TYPE_VENDOR = (0x02 << 5), + + /** Reserved */ + LIBUSB_REQUEST_TYPE_RESERVED = (0x03 << 5) +}; + +/** \ingroup libusb_misc + * Recipient bits of the + * \ref libusb_control_setup::bmRequestType "bmRequestType" field in control + * transfers. Values 4 through 31 are reserved. */ +enum libusb_request_recipient { + /** Device */ + LIBUSB_RECIPIENT_DEVICE = 0x00, + + /** Interface */ + LIBUSB_RECIPIENT_INTERFACE = 0x01, + + /** Endpoint */ + LIBUSB_RECIPIENT_ENDPOINT = 0x02, + + /** Other */ + LIBUSB_RECIPIENT_OTHER = 0x03 +}; + +#define LIBUSB_ISO_SYNC_TYPE_MASK 0x0c + +/** \ingroup libusb_desc + * Synchronization type for isochronous endpoints. Values for bits 2:3 of the + * \ref libusb_endpoint_descriptor::bmAttributes "bmAttributes" field in + * libusb_endpoint_descriptor. + */ +enum libusb_iso_sync_type { + /** No synchronization */ + LIBUSB_ISO_SYNC_TYPE_NONE = 0x0, + + /** Asynchronous */ + LIBUSB_ISO_SYNC_TYPE_ASYNC = 0x1, + + /** Adaptive */ + LIBUSB_ISO_SYNC_TYPE_ADAPTIVE = 0x2, + + /** Synchronous */ + LIBUSB_ISO_SYNC_TYPE_SYNC = 0x3 +}; + +#define LIBUSB_ISO_USAGE_TYPE_MASK 0x30 + +/** \ingroup libusb_desc + * Usage type for isochronous endpoints. Values for bits 4:5 of the + * \ref libusb_endpoint_descriptor::bmAttributes "bmAttributes" field in + * libusb_endpoint_descriptor. + */ +enum libusb_iso_usage_type { + /** Data endpoint */ + LIBUSB_ISO_USAGE_TYPE_DATA = 0x0, + + /** Feedback endpoint */ + LIBUSB_ISO_USAGE_TYPE_FEEDBACK = 0x1, + + /** Implicit feedback Data endpoint */ + LIBUSB_ISO_USAGE_TYPE_IMPLICIT = 0x2 +}; + +/** \ingroup libusb_desc + * Supported speeds (wSpeedSupported) bitfield. Indicates what + * speeds the device supports. + */ +enum libusb_supported_speed { + /** Low speed operation supported (1.5MBit/s). */ + LIBUSB_LOW_SPEED_OPERATION = (1 << 0), + + /** Full speed operation supported (12MBit/s). */ + LIBUSB_FULL_SPEED_OPERATION = (1 << 1), + + /** High speed operation supported (480MBit/s). */ + LIBUSB_HIGH_SPEED_OPERATION = (1 << 2), + + /** Superspeed operation supported (5000MBit/s). */ + LIBUSB_SUPER_SPEED_OPERATION = (1 << 3) +}; + +/** \ingroup libusb_desc + * Masks for the bits of the + * \ref libusb_usb_2_0_extension_descriptor::bmAttributes "bmAttributes" field + * of the USB 2.0 Extension descriptor. + */ +enum libusb_usb_2_0_extension_attributes { + /** Supports Link Power Management (LPM) */ + LIBUSB_BM_LPM_SUPPORT = (1 << 1) +}; + +/** \ingroup libusb_desc + * Masks for the bits of the + * \ref libusb_ss_usb_device_capability_descriptor::bmAttributes "bmAttributes" field + * field of the SuperSpeed USB Device Capability descriptor. + */ +enum libusb_ss_usb_device_capability_attributes { + /** Supports Latency Tolerance Messages (LTM) */ + LIBUSB_BM_LTM_SUPPORT = (1 << 1) +}; + +/** \ingroup libusb_desc + * USB capability types + */ +enum libusb_bos_type { + /** Wireless USB device capability */ + LIBUSB_BT_WIRELESS_USB_DEVICE_CAPABILITY = 0x01, + + /** USB 2.0 extensions */ + LIBUSB_BT_USB_2_0_EXTENSION = 0x02, + + /** SuperSpeed USB device capability */ + LIBUSB_BT_SS_USB_DEVICE_CAPABILITY = 0x03, + + /** Container ID type */ + LIBUSB_BT_CONTAINER_ID = 0x04, + + /** Platform descriptor */ + LIBUSB_BT_PLATFORM_DESCRIPTOR = 0x05, + + /** SuperSpeedPlus device capability */ + LIBUSB_BT_SUPERSPEED_PLUS_CAPABILITY = 0x0A, +}; + +/** \ingroup libusb_desc + * A structure representing the standard USB device descriptor. This + * descriptor is documented in section 9.6.1 of the USB 3.0 specification. + * All multiple-byte fields are represented in host-endian format. + */ +struct libusb_device_descriptor { + /** Size of this descriptor (in bytes) */ + uint8_t bLength; + + /** Descriptor type. Will have value + * \ref libusb_descriptor_type::LIBUSB_DT_DEVICE LIBUSB_DT_DEVICE in this + * context. */ + uint8_t bDescriptorType; + + /** USB specification release number in binary-coded decimal. A value of + * 0x0200 indicates USB 2.0, 0x0110 indicates USB 1.1, etc. */ + uint16_t bcdUSB; + + /** USB-IF class code for the device. See \ref libusb_class_code. */ + uint8_t bDeviceClass; + + /** USB-IF subclass code for the device, qualified by the bDeviceClass + * value */ + uint8_t bDeviceSubClass; + + /** USB-IF protocol code for the device, qualified by the bDeviceClass and + * bDeviceSubClass values */ + uint8_t bDeviceProtocol; + + /** Maximum packet size for endpoint 0 */ + uint8_t bMaxPacketSize0; + + /** USB-IF vendor ID */ + uint16_t idVendor; + + /** USB-IF product ID */ + uint16_t idProduct; + + /** Device release number in binary-coded decimal */ + uint16_t bcdDevice; + + /** Index of string descriptor describing manufacturer */ + uint8_t iManufacturer; + + /** Index of string descriptor describing product */ + uint8_t iProduct; + + /** Index of string descriptor containing device serial number */ + uint8_t iSerialNumber; + + /** Number of possible configurations */ + uint8_t bNumConfigurations; +}; + +/** \ingroup libusb_desc + * A structure representing the standard USB endpoint descriptor. This + * descriptor is documented in section 9.6.6 of the USB 3.0 specification. + * All multiple-byte fields are represented in host-endian format. + */ +struct libusb_endpoint_descriptor { + /** Size of this descriptor (in bytes) */ + uint8_t bLength; + + /** Descriptor type. Will have value + * \ref libusb_descriptor_type::LIBUSB_DT_ENDPOINT LIBUSB_DT_ENDPOINT in + * this context. */ + uint8_t bDescriptorType; + + /** The address of the endpoint described by this descriptor. Bits 0:3 are + * the endpoint number. Bits 4:6 are reserved. Bit 7 indicates direction, + * see \ref libusb_endpoint_direction. */ + uint8_t bEndpointAddress; + + /** Attributes which apply to the endpoint when it is configured using + * the bConfigurationValue. Bits 0:1 determine the transfer type and + * correspond to \ref libusb_endpoint_transfer_type. Bits 2:3 are only used + * for isochronous endpoints and correspond to \ref libusb_iso_sync_type. + * Bits 4:5 are also only used for isochronous endpoints and correspond to + * \ref libusb_iso_usage_type. Bits 6:7 are reserved. */ + uint8_t bmAttributes; + + /** Maximum packet size this endpoint is capable of sending/receiving. */ + uint16_t wMaxPacketSize; + + /** Interval for polling endpoint for data transfers. */ + uint8_t bInterval; + + /** For audio devices only: the rate at which synchronization feedback + * is provided. */ + uint8_t bRefresh; + + /** For audio devices only: the address if the synch endpoint */ + uint8_t bSynchAddress; + + /** Extra descriptors. If libusb encounters unknown endpoint descriptors, + * it will store them here, should you wish to parse them. */ + const unsigned char *extra; + + /** Length of the extra descriptors, in bytes. Must be non-negative. */ + int extra_length; +}; + +/** \ingroup libusb_desc + * A structure representing the standard USB interface association descriptor. + * This descriptor is documented in section 9.6.4 of the USB 3.0 specification. + * All multiple-byte fields are represented in host-endian format. + */ +struct libusb_interface_association_descriptor { + /** Size of this descriptor (in bytes) */ + uint8_t bLength; + + /** Descriptor type. Will have value + * \ref libusb_descriptor_type::LIBUSB_DT_INTERFACE_ASSOCIATION + * LIBUSB_DT_INTERFACE_ASSOCIATION in this context. */ + uint8_t bDescriptorType; + + /** Interface number of the first interface that is associated + * with this function */ + uint8_t bFirstInterface; + + /** Number of contiguous interfaces that are associated with + * this function */ + uint8_t bInterfaceCount; + + /** USB-IF class code for this function. + * A value of zero is not allowed in this descriptor. + * If this field is 0xff, the function class is vendor-specific. + * All other values are reserved for assignment by the USB-IF. + */ + uint8_t bFunctionClass; + + /** USB-IF subclass code for this function. + * If this field is not set to 0xff, all values are reserved + * for assignment by the USB-IF + */ + uint8_t bFunctionSubClass; + + /** USB-IF protocol code for this function. + * These codes are qualified by the values of the bFunctionClass + * and bFunctionSubClass fields. + */ + uint8_t bFunctionProtocol; + + /** Index of string descriptor describing this function */ + uint8_t iFunction; +}; + +/** \ingroup libusb_desc + * Structure containing an array of 0 or more interface association + * descriptors + */ +struct libusb_interface_association_descriptor_array { + /** Array of interface association descriptors. The size of this array + * is determined by the length field. + */ + const struct libusb_interface_association_descriptor *iad; + + /** Number of interface association descriptors contained. Read-only. */ + int length; +}; + +/** \ingroup libusb_desc + * A structure representing the standard USB interface descriptor. This + * descriptor is documented in section 9.6.5 of the USB 3.0 specification. + * All multiple-byte fields are represented in host-endian format. + */ +struct libusb_interface_descriptor { + /** Size of this descriptor (in bytes) */ + uint8_t bLength; + + /** Descriptor type. Will have value + * \ref libusb_descriptor_type::LIBUSB_DT_INTERFACE LIBUSB_DT_INTERFACE + * in this context. */ + uint8_t bDescriptorType; + + /** Number of this interface */ + uint8_t bInterfaceNumber; + + /** Value used to select this alternate setting for this interface */ + uint8_t bAlternateSetting; + + /** Number of endpoints used by this interface (excluding the control + * endpoint). */ + uint8_t bNumEndpoints; + + /** USB-IF class code for this interface. See \ref libusb_class_code. */ + uint8_t bInterfaceClass; + + /** USB-IF subclass code for this interface, qualified by the + * bInterfaceClass value */ + uint8_t bInterfaceSubClass; + + /** USB-IF protocol code for this interface, qualified by the + * bInterfaceClass and bInterfaceSubClass values */ + uint8_t bInterfaceProtocol; + + /** Index of string descriptor describing this interface */ + uint8_t iInterface; + + /** Array of endpoint descriptors. This length of this array is determined + * by the bNumEndpoints field. */ + const struct libusb_endpoint_descriptor *endpoint; + + /** Extra descriptors. If libusb encounters unknown interface descriptors, + * it will store them here, should you wish to parse them. */ + const unsigned char *extra; + + /** Length of the extra descriptors, in bytes. Must be non-negative. */ + int extra_length; +}; + +/** \ingroup libusb_desc + * A collection of alternate settings for a particular USB interface. + */ +struct libusb_interface { + /** Array of interface descriptors. The length of this array is determined + * by the num_altsetting field. */ + const struct libusb_interface_descriptor *altsetting; + + /** The number of alternate settings that belong to this interface. + * Must be non-negative. */ + int num_altsetting; +}; + +/** \ingroup libusb_desc + * A structure representing the standard USB configuration descriptor. This + * descriptor is documented in section 9.6.3 of the USB 3.0 specification. + * All multiple-byte fields are represented in host-endian format. + */ +struct libusb_config_descriptor { + /** Size of this descriptor (in bytes) */ + uint8_t bLength; + + /** Descriptor type. Will have value + * \ref libusb_descriptor_type::LIBUSB_DT_CONFIG LIBUSB_DT_CONFIG + * in this context. */ + uint8_t bDescriptorType; + + /** Total length of data returned for this configuration */ + uint16_t wTotalLength; + + /** Number of interfaces supported by this configuration */ + uint8_t bNumInterfaces; + + /** Identifier value for this configuration */ + uint8_t bConfigurationValue; + + /** Index of string descriptor describing this configuration */ + uint8_t iConfiguration; + + /** Configuration characteristics */ + uint8_t bmAttributes; + + /** Maximum power consumption of the USB device from this bus in this + * configuration when the device is fully operation. Expressed in units + * of 2 mA when the device is operating in high-speed mode and in units + * of 8 mA when the device is operating in super-speed mode. */ + uint8_t MaxPower; + + /** Array of interfaces supported by this configuration. The length of + * this array is determined by the bNumInterfaces field. */ + const struct libusb_interface *interface; + + /** Extra descriptors. If libusb encounters unknown configuration + * descriptors, it will store them here, should you wish to parse them. */ + const unsigned char *extra; + + /** Length of the extra descriptors, in bytes. Must be non-negative. */ + int extra_length; +}; + +/** \ingroup libusb_desc + * A structure representing the superspeed endpoint companion + * descriptor. This descriptor is documented in section 9.6.7 of + * the USB 3.0 specification. All multiple-byte fields are represented in + * host-endian format. + */ +struct libusb_ss_endpoint_companion_descriptor { + /** Size of this descriptor (in bytes) */ + uint8_t bLength; + + /** Descriptor type. Will have value + * \ref libusb_descriptor_type::LIBUSB_DT_SS_ENDPOINT_COMPANION in + * this context. */ + uint8_t bDescriptorType; + + /** The maximum number of packets the endpoint can send or + * receive as part of a burst. */ + uint8_t bMaxBurst; + + /** In bulk EP: bits 4:0 represents the maximum number of + * streams the EP supports. In isochronous EP: bits 1:0 + * represents the Mult - a zero based value that determines + * the maximum number of packets within a service interval */ + uint8_t bmAttributes; + + /** The total number of bytes this EP will transfer every + * service interval. Valid only for periodic EPs. */ + uint16_t wBytesPerInterval; +}; + +/** \ingroup libusb_desc + * A generic representation of a BOS Device Capability descriptor. It is + * advised to check bDevCapabilityType and call the matching + * libusb_get_*_descriptor function to get a structure fully matching the type. + */ +struct libusb_bos_dev_capability_descriptor { + /** Size of this descriptor (in bytes) */ + uint8_t bLength; + + /** Descriptor type. Will have value + * \ref libusb_descriptor_type::LIBUSB_DT_DEVICE_CAPABILITY + * LIBUSB_DT_DEVICE_CAPABILITY in this context. */ + uint8_t bDescriptorType; + + /** Device Capability type */ + uint8_t bDevCapabilityType; + + /** Device Capability data (bLength - 3 bytes) */ + uint8_t dev_capability_data[LIBUSB_FLEXIBLE_ARRAY]; +}; + +/** \ingroup libusb_desc + * A structure representing the Binary Device Object Store (BOS) descriptor. + * This descriptor is documented in section 9.6.2 of the USB 3.0 specification. + * All multiple-byte fields are represented in host-endian format. + */ +struct libusb_bos_descriptor { + /** Size of this descriptor (in bytes) */ + uint8_t bLength; + + /** Descriptor type. Will have value + * \ref libusb_descriptor_type::LIBUSB_DT_BOS LIBUSB_DT_BOS + * in this context. */ + uint8_t bDescriptorType; + + /** Length of this descriptor and all of its sub descriptors */ + uint16_t wTotalLength; + + /** The number of separate device capability descriptors in + * the BOS */ + uint8_t bNumDeviceCaps; + + /** bNumDeviceCap Device Capability Descriptors */ + struct libusb_bos_dev_capability_descriptor *dev_capability[LIBUSB_FLEXIBLE_ARRAY]; +}; + +/** \ingroup libusb_desc + * A structure representing the USB 2.0 Extension descriptor + * This descriptor is documented in section 9.6.2.1 of the USB 3.0 specification. + * All multiple-byte fields are represented in host-endian format. + */ +struct libusb_usb_2_0_extension_descriptor { + /** Size of this descriptor (in bytes) */ + uint8_t bLength; + + /** Descriptor type. Will have value + * \ref libusb_descriptor_type::LIBUSB_DT_DEVICE_CAPABILITY + * LIBUSB_DT_DEVICE_CAPABILITY in this context. */ + uint8_t bDescriptorType; + + /** Capability type. Will have value + * \ref libusb_bos_type::LIBUSB_BT_USB_2_0_EXTENSION + * LIBUSB_BT_USB_2_0_EXTENSION in this context. */ + uint8_t bDevCapabilityType; + + /** Bitmap encoding of supported device level features. + * A value of one in a bit location indicates a feature is + * supported; a value of zero indicates it is not supported. + * See \ref libusb_usb_2_0_extension_attributes. */ + uint32_t bmAttributes; +}; + +/** \ingroup libusb_desc + * A structure representing the SuperSpeed USB Device Capability descriptor + * This descriptor is documented in section 9.6.2.2 of the USB 3.0 specification. + * All multiple-byte fields are represented in host-endian format. + */ +struct libusb_ss_usb_device_capability_descriptor { + /** Size of this descriptor (in bytes) */ + uint8_t bLength; + + /** Descriptor type. Will have value + * \ref libusb_descriptor_type::LIBUSB_DT_DEVICE_CAPABILITY + * LIBUSB_DT_DEVICE_CAPABILITY in this context. */ + uint8_t bDescriptorType; + + /** Capability type. Will have value + * \ref libusb_bos_type::LIBUSB_BT_SS_USB_DEVICE_CAPABILITY + * LIBUSB_BT_SS_USB_DEVICE_CAPABILITY in this context. */ + uint8_t bDevCapabilityType; + + /** Bitmap encoding of supported device level features. + * A value of one in a bit location indicates a feature is + * supported; a value of zero indicates it is not supported. + * See \ref libusb_ss_usb_device_capability_attributes. */ + uint8_t bmAttributes; + + /** Bitmap encoding of the speed supported by this device when + * operating in SuperSpeed mode. See \ref libusb_supported_speed. */ + uint16_t wSpeedSupported; + + /** The lowest speed at which all the functionality supported + * by the device is available to the user. For example if the + * device supports all its functionality when connected at + * full speed and above then it sets this value to 1. */ + uint8_t bFunctionalitySupport; + + /** U1 Device Exit Latency. */ + uint8_t bU1DevExitLat; + + /** U2 Device Exit Latency. */ + uint16_t bU2DevExitLat; +}; + +/** \ingroup libusb_desc + * enum used in \ref libusb_ssplus_sublink_attribute + */ +enum libusb_superspeedplus_sublink_attribute_sublink_type { + LIBUSB_SSPLUS_ATTR_TYPE_SYM = 0, + LIBUSB_SSPLUS_ATTR_TYPE_ASYM = 1, +}; + +/** \ingroup libusb_desc + * enum used in \ref libusb_ssplus_sublink_attribute + */ +enum libusb_superspeedplus_sublink_attribute_sublink_direction { + LIBUSB_SSPLUS_ATTR_DIR_RX = 0, + LIBUSB_SSPLUS_ATTR_DIR_TX = 1, +}; + +/** \ingroup libusb_desc + * enum used in \ref libusb_ssplus_sublink_attribute + * Bit = Bits per second + * Kb = Kbps + * Mb = Mbps + * Gb = Gbps + */ +enum libusb_superspeedplus_sublink_attribute_exponent { + LIBUSB_SSPLUS_ATTR_EXP_BPS = 0, + LIBUSB_SSPLUS_ATTR_EXP_KBS = 1, + LIBUSB_SSPLUS_ATTR_EXP_MBS = 2, + LIBUSB_SSPLUS_ATTR_EXP_GBS = 3, +}; + +/** \ingroup libusb_desc + * enum used in \ref libusb_ssplus_sublink_attribute + */ +enum libusb_superspeedplus_sublink_attribute_link_protocol { + LIBUSB_SSPLUS_ATTR_PROT_SS = 0, + LIBUSB_SSPLUS_ATTR_PROT_SSPLUS = 1, +}; + +/** \ingroup libusb_desc + * Expose \ref libusb_ssplus_usb_device_capability_descriptor.sublinkSpeedAttributes + */ +struct libusb_ssplus_sublink_attribute { + /** Sublink Speed Attribute ID (SSID). + This field is an ID that uniquely identifies the speed of this sublink */ + uint8_t ssid; + + /** This field defines the + base 10 exponent times 3, that shall be applied to the + mantissa. */ + enum libusb_superspeedplus_sublink_attribute_exponent exponent; + + /** This field identifies whether the + Sublink Speed Attribute defines a symmetric or + asymmetric bit rate.*/ + enum libusb_superspeedplus_sublink_attribute_sublink_type type; + + /** This field indicates if this + Sublink Speed Attribute defines the receive or + transmit bit rate. */ + enum libusb_superspeedplus_sublink_attribute_sublink_direction direction; + + /** This field identifies the protocol + supported by the link. */ + enum libusb_superspeedplus_sublink_attribute_link_protocol protocol; + + /** This field defines the mantissa that shall be applied to the exponent when + calculating the maximum bit rate. */ + uint16_t mantissa; +}; + +/** \ingroup libusb_desc + * A structure representing the SuperSpeedPlus descriptor + * This descriptor is documented in section 9.6.2.5 of the USB 3.1 specification. + */ +struct libusb_ssplus_usb_device_capability_descriptor { + /** Sublink Speed Attribute Count */ + uint8_t numSublinkSpeedAttributes; + + /** Sublink Speed ID Count */ + uint8_t numSublinkSpeedIDs; + + /** Unique ID to indicates the minimum lane speed */ + uint8_t ssid; + + /** This field indicates the minimum receive lane count.*/ + uint8_t minRxLaneCount; + + /** This field indicates the minimum transmit lane count*/ + uint8_t minTxLaneCount; + + /** Array size is \ref libusb_ssplus_usb_device_capability_descriptor.numSublinkSpeedAttributes */ + struct libusb_ssplus_sublink_attribute sublinkSpeedAttributes[]; +}; + +/** \ingroup libusb_desc + * A structure representing the Container ID descriptor. + * This descriptor is documented in section 9.6.2.3 of the USB 3.0 specification. + * All multiple-byte fields, except UUIDs, are represented in host-endian format. + */ +struct libusb_container_id_descriptor { + /** Size of this descriptor (in bytes) */ + uint8_t bLength; + + /** Descriptor type. Will have value + * \ref libusb_descriptor_type::LIBUSB_DT_DEVICE_CAPABILITY + * LIBUSB_DT_DEVICE_CAPABILITY in this context. */ + uint8_t bDescriptorType; + + /** Capability type. Will have value + * \ref libusb_bos_type::LIBUSB_BT_CONTAINER_ID + * LIBUSB_BT_CONTAINER_ID in this context. */ + uint8_t bDevCapabilityType; + + /** Reserved field */ + uint8_t bReserved; + + /** 128 bit UUID */ + uint8_t ContainerID[16]; +}; + +/** \ingroup libusb_desc + * A structure representing a Platform descriptor. + * This descriptor is documented in section 9.6.2.4 of the USB 3.2 specification. + */ +struct libusb_platform_descriptor { + /** Size of this descriptor (in bytes) */ + uint8_t bLength; + + /** Descriptor type. Will have value + * \ref libusb_descriptor_type::LIBUSB_DT_DEVICE_CAPABILITY + * LIBUSB_DT_DEVICE_CAPABILITY in this context. */ + uint8_t bDescriptorType; + + /** Capability type. Will have value + * \ref libusb_bos_type::LIBUSB_BT_PLATFORM_DESCRIPTOR + * LIBUSB_BT_CONTAINER_ID in this context. */ + uint8_t bDevCapabilityType; + + /** Reserved field */ + uint8_t bReserved; + + /** 128 bit UUID */ + uint8_t PlatformCapabilityUUID[16]; + + /** Capability data (bLength - 20) */ + uint8_t CapabilityData[LIBUSB_FLEXIBLE_ARRAY]; +}; + +/** \ingroup libusb_asyncio + * Setup packet for control transfers. */ +#if defined(_MSC_VER) || defined(__WATCOMC__) +#pragma pack(push, 1) +#endif +struct libusb_control_setup { + /** Request type. Bits 0:4 determine recipient, see + * \ref libusb_request_recipient. Bits 5:6 determine type, see + * \ref libusb_request_type. Bit 7 determines data transfer direction, see + * \ref libusb_endpoint_direction. + */ + uint8_t bmRequestType; + + /** Request. If the type bits of bmRequestType are equal to + * \ref libusb_request_type::LIBUSB_REQUEST_TYPE_STANDARD + * "LIBUSB_REQUEST_TYPE_STANDARD" then this field refers to + * \ref libusb_standard_request. For other cases, use of this field is + * application-specific. */ + uint8_t bRequest; + + /** Value. Varies according to request */ + uint16_t wValue; + + /** Index. Varies according to request, typically used to pass an index + * or offset */ + uint16_t wIndex; + + /** Number of bytes to transfer */ + uint16_t wLength; +} LIBUSB_PACKED; +#if defined(_MSC_VER) || defined(__WATCOMC__) +#pragma pack(pop) +#endif + +#define LIBUSB_CONTROL_SETUP_SIZE (sizeof(struct libusb_control_setup)) + +/* libusb */ + +struct libusb_context; +struct libusb_device; +struct libusb_device_handle; + +/** \ingroup libusb_lib + * Structure providing the version of the libusb runtime + */ +struct libusb_version { + /** Library major version. */ + uint16_t major; + + /** Library minor version. */ + uint16_t minor; + + /** Library micro version. */ + uint16_t micro; + + /** Library nano version. */ + uint16_t nano; + + /** Library release candidate suffix string, e.g. "-rc4". */ + const char *rc; + + /** For ABI compatibility only. */ + const char *describe; +}; + +/** \ingroup libusb_lib + * Structure representing a libusb session. The concept of individual libusb + * sessions allows for your program to use two libraries (or dynamically + * load two modules) which both independently use libusb. This will prevent + * interference between the individual libusb users - for example + * libusb_set_option() will not affect the other user of the library, and + * libusb_exit() will not destroy resources that the other user is still + * using. + * + * Sessions are created by libusb_init_context() and destroyed through libusb_exit(). + * If your application is guaranteed to only ever include a single libusb + * user (i.e. you), you do not have to worry about contexts: pass NULL in + * every function call where a context is required, and the default context + * will be used. Note that libusb_set_option(NULL, ...) is special, and adds + * an option to a list of default options for new contexts. + * + * For more information, see \ref libusb_contexts. + */ +typedef struct libusb_context libusb_context; + +/** \ingroup libusb_dev + * Structure representing a USB device detected on the system. This is an + * opaque type for which you are only ever provided with a pointer, usually + * originating from libusb_get_device_list() or libusb_hotplug_register_callback(). + * + * Certain operations can be performed on a device, but in order to do any + * I/O you will have to first obtain a device handle using libusb_open(). + * + * Devices are reference counted with libusb_ref_device() and + * libusb_unref_device(), and are freed when the reference count reaches 0. + * New devices presented by libusb_get_device_list() have a reference count of + * 1, and libusb_free_device_list() can optionally decrease the reference count + * on all devices in the list. libusb_open() adds another reference which is + * later destroyed by libusb_close(). + */ +typedef struct libusb_device libusb_device; + + +/** \ingroup libusb_dev + * Structure representing a handle on a USB device. This is an opaque type for + * which you are only ever provided with a pointer, usually originating from + * libusb_open(). + * + * A device handle is used to perform I/O and other operations. When finished + * with a device handle, you should call libusb_close(). + */ +typedef struct libusb_device_handle libusb_device_handle; + +/** \ingroup libusb_dev + * Speed codes. Indicates the speed at which the device is operating. + */ +enum libusb_speed { + /** The OS doesn't report or know the device speed. */ + LIBUSB_SPEED_UNKNOWN = 0, + + /** The device is operating at low speed (1.5MBit/s). */ + LIBUSB_SPEED_LOW = 1, + + /** The device is operating at full speed (12MBit/s). */ + LIBUSB_SPEED_FULL = 2, + + /** The device is operating at high speed (480MBit/s). */ + LIBUSB_SPEED_HIGH = 3, + + /** The device is operating at super speed (5000MBit/s). */ + LIBUSB_SPEED_SUPER = 4, + + /** The device is operating at super speed plus (10000MBit/s). */ + LIBUSB_SPEED_SUPER_PLUS = 5, + + /** The device is operating at super speed plus x2 (20000MBit/s). */ + LIBUSB_SPEED_SUPER_PLUS_X2 = 6, +}; + +/** \ingroup libusb_misc + * Error codes. Most libusb functions return 0 on success or one of these + * codes on failure. + * You can call libusb_error_name() to retrieve a string representation of an + * error code or libusb_strerror() to get an end-user suitable description of + * an error code. + */ +enum libusb_error { + /** Success (no error) */ + LIBUSB_SUCCESS = 0, + + /** Input/output error */ + LIBUSB_ERROR_IO = -1, + + /** Invalid parameter */ + LIBUSB_ERROR_INVALID_PARAM = -2, + + /** Access denied (insufficient permissions) */ + LIBUSB_ERROR_ACCESS = -3, + + /** No such device (it may have been disconnected) */ + LIBUSB_ERROR_NO_DEVICE = -4, + + /** Entity not found */ + LIBUSB_ERROR_NOT_FOUND = -5, + + /** Resource busy */ + LIBUSB_ERROR_BUSY = -6, + + /** Operation timed out */ + LIBUSB_ERROR_TIMEOUT = -7, + + /** Overflow */ + LIBUSB_ERROR_OVERFLOW = -8, + + /** Pipe error */ + LIBUSB_ERROR_PIPE = -9, + + /** System call interrupted (perhaps due to signal) */ + LIBUSB_ERROR_INTERRUPTED = -10, + + /** Insufficient memory */ + LIBUSB_ERROR_NO_MEM = -11, + + /** Operation not supported or unimplemented on this platform */ + LIBUSB_ERROR_NOT_SUPPORTED = -12, + + /* NB: Remember to update LIBUSB_ERROR_COUNT below as well as the + message strings in strerror.c when adding new error codes here. */ + + /** Other error */ + LIBUSB_ERROR_OTHER = -99 +}; + +/* Total number of error codes in enum libusb_error */ +#define LIBUSB_ERROR_COUNT 14 + +/** \ingroup libusb_asyncio + * Transfer type */ +enum libusb_transfer_type { + /** Control transfer */ + LIBUSB_TRANSFER_TYPE_CONTROL = 0U, + + /** Isochronous transfer */ + LIBUSB_TRANSFER_TYPE_ISOCHRONOUS = 1U, + + /** Bulk transfer */ + LIBUSB_TRANSFER_TYPE_BULK = 2U, + + /** Interrupt transfer */ + LIBUSB_TRANSFER_TYPE_INTERRUPT = 3U, + + /** Bulk stream transfer */ + LIBUSB_TRANSFER_TYPE_BULK_STREAM = 4U +}; + +/** \ingroup libusb_asyncio + * Transfer status codes */ +enum libusb_transfer_status { + /** Transfer completed without error. Note that this does not indicate + * that the entire amount of requested data was transferred. */ + LIBUSB_TRANSFER_COMPLETED, + + /** Transfer failed */ + LIBUSB_TRANSFER_ERROR, + + /** Transfer timed out */ + LIBUSB_TRANSFER_TIMED_OUT, + + /** Transfer was cancelled */ + LIBUSB_TRANSFER_CANCELLED, + + /** For bulk/interrupt endpoints: halt condition detected (endpoint + * stalled). For control endpoints: control request not supported. */ + LIBUSB_TRANSFER_STALL, + + /** Device was disconnected */ + LIBUSB_TRANSFER_NO_DEVICE, + + /** Device sent more data than requested */ + LIBUSB_TRANSFER_OVERFLOW + + /* NB! Remember to update libusb_error_name() + when adding new status codes here. */ +}; + +/** \ingroup libusb_asyncio + * libusb_transfer.flags values */ +enum libusb_transfer_flags { + /** Report short frames as errors */ + LIBUSB_TRANSFER_SHORT_NOT_OK = (1U << 0), + + /** Automatically free() transfer buffer during libusb_free_transfer(). + * Note that buffers allocated with libusb_dev_mem_alloc() should not + * be attempted freed in this way, since free() is not an appropriate + * way to release such memory. */ + LIBUSB_TRANSFER_FREE_BUFFER = (1U << 1), + + /** Automatically call libusb_free_transfer() after callback returns. + * If this flag is set, it is illegal to call libusb_free_transfer() + * from your transfer callback, as this will result in a double-free + * when this flag is acted upon. */ + LIBUSB_TRANSFER_FREE_TRANSFER = (1U << 2), + + /** Terminate transfers that are a multiple of the endpoint's + * wMaxPacketSize with an extra zero length packet. This is useful + * when a device protocol mandates that each logical request is + * terminated by an incomplete packet (i.e. the logical requests are + * not separated by other means). + * + * This flag only affects host-to-device transfers to bulk and interrupt + * endpoints. In other situations, it is ignored. + * + * This flag only affects transfers with a length that is a multiple of + * the endpoint's wMaxPacketSize. On transfers of other lengths, this + * flag has no effect. Therefore, if you are working with a device that + * needs a ZLP whenever the end of the logical request falls on a packet + * boundary, then it is sensible to set this flag on every + * transfer (you do not have to worry about only setting it on transfers + * that end on the boundary). + * + * This flag is currently only supported on Linux. + * On other systems, libusb_submit_transfer() will return + * \ref LIBUSB_ERROR_NOT_SUPPORTED for every transfer where this + * flag is set. + * + * Available since libusb-1.0.9. + */ + LIBUSB_TRANSFER_ADD_ZERO_PACKET = (1U << 3) +}; + +/** \ingroup libusb_asyncio + * Isochronous packet descriptor. */ +struct libusb_iso_packet_descriptor { + /** Length of data to request in this packet */ + unsigned int length; + + /** Amount of data that was actually transferred */ + unsigned int actual_length; + + /** Status code for this packet */ + enum libusb_transfer_status status; +}; + +struct libusb_transfer; + +/** \ingroup libusb_asyncio + * Asynchronous transfer callback function type. When submitting asynchronous + * transfers, you pass a pointer to a callback function of this type via the + * \ref libusb_transfer::callback "callback" member of the libusb_transfer + * structure. libusb will call this function later, when the transfer has + * completed or failed. See \ref libusb_asyncio for more information. + * \param transfer The libusb_transfer struct the callback function is being + * notified about. + */ +typedef void (LIBUSB_CALL *libusb_transfer_cb_fn)(struct libusb_transfer *transfer); + +/** \ingroup libusb_asyncio + * The generic USB transfer structure. The user populates this structure and + * then submits it in order to request a transfer. After the transfer has + * completed, the library populates the transfer with the results and passes + * it back to the user. + */ +struct libusb_transfer { + /** Handle of the device that this transfer will be submitted to */ + libusb_device_handle *dev_handle; + + /** A bitwise OR combination of \ref libusb_transfer_flags. */ + uint8_t flags; + + /** Address of the endpoint where this transfer will be sent. */ + unsigned char endpoint; + + /** Type of the transfer from \ref libusb_transfer_type */ + unsigned char type; + + /** Timeout for this transfer in milliseconds. A value of 0 indicates no + * timeout. */ + unsigned int timeout; + + /** The status of the transfer. Read-only, and only for use within + * transfer callback function. + * + * If this is an isochronous transfer, this field may read COMPLETED even + * if there were errors in the frames. Use the + * \ref libusb_iso_packet_descriptor::status "status" field in each packet + * to determine if errors occurred. */ + enum libusb_transfer_status status; + + /** Length of the data buffer. Must be non-negative. */ + int length; + + /** Actual length of data that was transferred. Read-only, and only for + * use within transfer callback function. Not valid for isochronous + * endpoint transfers. */ + int actual_length; + + /** Callback function. This will be invoked when the transfer completes, + * fails, or is cancelled. */ + libusb_transfer_cb_fn callback; + + /** User context data. Useful for associating specific data to a transfer + * that can be accessed from within the callback function. + * + * This field may be set manually or is taken as the `user_data` parameter + * of the following functions: + * - libusb_fill_bulk_transfer() + * - libusb_fill_bulk_stream_transfer() + * - libusb_fill_control_transfer() + * - libusb_fill_interrupt_transfer() + * - libusb_fill_iso_transfer() */ + void *user_data; + + /** Data buffer */ + unsigned char *buffer; + + /** Number of isochronous packets. Only used for I/O with isochronous + * endpoints. Must be non-negative. */ + int num_iso_packets; + + /** Isochronous packet descriptors, for isochronous transfers only. */ + struct libusb_iso_packet_descriptor iso_packet_desc[LIBUSB_FLEXIBLE_ARRAY]; +}; + +/** \ingroup libusb_misc + * Capabilities supported by an instance of libusb on the current running + * platform. Test if the loaded library supports a given capability by calling + * \ref libusb_has_capability(). + */ +enum libusb_capability { + /** The libusb_has_capability() API is available. */ + LIBUSB_CAP_HAS_CAPABILITY = 0x0000U, + + /** Hotplug support is available on this platform. */ + LIBUSB_CAP_HAS_HOTPLUG = 0x0001U, + + /** The library can access HID devices without requiring user intervention. + * Note that before being able to actually access an HID device, you may + * still have to call additional libusb functions such as + * \ref libusb_detach_kernel_driver(). */ + LIBUSB_CAP_HAS_HID_ACCESS = 0x0100U, + + /** The library supports detaching of the default USB driver, using + * \ref libusb_detach_kernel_driver(), if one is set by the OS kernel */ + LIBUSB_CAP_SUPPORTS_DETACH_KERNEL_DRIVER = 0x0101U +}; + +/** \ingroup libusb_lib + * Log message levels. + */ +enum libusb_log_level { + /** (0) : No messages ever emitted by the library (default) */ + LIBUSB_LOG_LEVEL_NONE = 0, + + /** (1) : Error messages are emitted */ + LIBUSB_LOG_LEVEL_ERROR = 1, + + /** (2) : Warning and error messages are emitted */ + LIBUSB_LOG_LEVEL_WARNING = 2, + + /** (3) : Informational, warning and error messages are emitted */ + LIBUSB_LOG_LEVEL_INFO = 3, + + /** (4) : All messages are emitted */ + LIBUSB_LOG_LEVEL_DEBUG = 4 +}; + +/** \ingroup libusb_lib + * Log callback mode. + * + * Since version 1.0.23, \ref LIBUSB_API_VERSION >= 0x01000107 + * + * \see libusb_set_log_cb() + */ +enum libusb_log_cb_mode { + /** Callback function handling all log messages. */ + LIBUSB_LOG_CB_GLOBAL = (1 << 0), + + /** Callback function handling context related log messages. */ + LIBUSB_LOG_CB_CONTEXT = (1 << 1) +}; + +/** \ingroup libusb_lib + * Available option values for libusb_set_option() and libusb_init_context(). + */ +enum libusb_option { + /** Set the log message verbosity. + * + * This option must be provided an argument of type \ref libusb_log_level. + * The default level is LIBUSB_LOG_LEVEL_NONE, which means no messages are ever + * printed. If you choose to increase the message verbosity level, ensure + * that your application does not close the stderr file descriptor. + * + * You are advised to use level LIBUSB_LOG_LEVEL_WARNING. libusb is conservative + * with its message logging and most of the time, will only log messages that + * explain error conditions and other oddities. This will help you debug + * your software. + * + * If the LIBUSB_DEBUG environment variable was set when libusb was + * initialized, this option does nothing: the message verbosity is fixed + * to the value in the environment variable. + * + * If libusb was compiled without any message logging, this option does + * nothing: you'll never get any messages. + * + * If libusb was compiled with verbose debug message logging, this option + * does nothing: you'll always get messages from all levels. + */ + LIBUSB_OPTION_LOG_LEVEL = 0, + + /** Use the UsbDk backend for a specific context, if available. + * + * This option should be set at initialization with libusb_init_context() + * otherwise unspecified behavior may occur. + * + * Only valid on Windows. Ignored on all other platforms. + */ + LIBUSB_OPTION_USE_USBDK = 1, + + /** Do not scan for devices + * + * With this option set, libusb will skip scanning devices in + * libusb_init_context(). + * + * Hotplug functionality will also be deactivated. + * + * The option is useful in combination with libusb_wrap_sys_device(), + * which can access a device directly without prior device scanning. + * + * This is typically needed on Android, where access to USB devices + * is limited. + * + * This option should only be used with libusb_init_context() + * otherwise unspecified behavior may occur. + * + * Only valid on Linux. Ignored on all other platforms. + */ + LIBUSB_OPTION_NO_DEVICE_DISCOVERY = 2, + +#define LIBUSB_OPTION_WEAK_AUTHORITY LIBUSB_OPTION_NO_DEVICE_DISCOVERY + + /** Set the context log callback function. + * + * Set the log callback function either on a context or globally. This + * option must be provided an argument of type \ref libusb_log_cb. + * Using this option with a NULL context is equivalent to calling + * libusb_set_log_cb() with mode \ref LIBUSB_LOG_CB_GLOBAL. + * Using it with a non-NULL context is equivalent to calling + * libusb_set_log_cb() with mode \ref LIBUSB_LOG_CB_CONTEXT. + */ + LIBUSB_OPTION_LOG_CB = 3, + + LIBUSB_OPTION_MAX = 4 +}; + +/** \ingroup libusb_desc + * The device string type. + */ +enum libusb_device_string_type { + LIBUSB_DEVICE_STRING_MANUFACTURER, + LIBUSB_DEVICE_STRING_PRODUCT, + LIBUSB_DEVICE_STRING_SERIAL_NUMBER, + LIBUSB_DEVICE_STRING_COUNT /* The total number of string types. */ +}; + +/** \ingroup libusb_desc + * The maximum length for a device string descriptor in UTF-8. + * + * 255 max descriptor length with 2 byte header + * => 253 bytes UTF-16LE, no null termination (USB 2.0 9.6.7) + * => 126.5 codepoints + * => 127 * 3 + 1 + * => 382 bytes + * + * Stay with 256 * 3/2 = 384 to be safe. + */ +#define LIBUSB_DEVICE_STRING_BYTES_MAX (384U) + +/** \ingroup libusb_lib + * Callback function for handling log messages. + * \param ctx the context which is related to the log message, or NULL if it + * is a global log message + * \param level the log level, see \ref libusb_log_level for a description + * \param str the log message + * + * Since version 1.0.23, \ref LIBUSB_API_VERSION >= 0x01000107 + * + * \see libusb_set_log_cb() + */ +typedef void (LIBUSB_CALL *libusb_log_cb)(libusb_context *ctx, + enum libusb_log_level level, const char *str); + +/** \ingroup libusb_lib + * Structure used for setting options through \ref libusb_init_context. + * + */ +struct libusb_init_option { + /** Which option to set */ + enum libusb_option option; + /** An integer value used by the option (if applicable). */ + union { + int ival; + libusb_log_cb log_cbval; + } value; +}; + +int LIBUSB_CALL libusb_init(libusb_context **ctx); +int LIBUSB_CALL libusb_init_context(libusb_context **ctx, const struct libusb_init_option options[], int num_options); +void LIBUSB_CALL libusb_exit(libusb_context *ctx); +void LIBUSB_CALL libusb_set_debug(libusb_context *ctx, int level); +/* may be deprecated in the future in favor of lubusb_init_context()+libusb_set_option() */ +void LIBUSB_CALL libusb_set_log_cb(libusb_context *ctx, libusb_log_cb cb, int mode); +const struct libusb_version * LIBUSB_CALL libusb_get_version(void); +int LIBUSB_CALL libusb_has_capability(uint32_t capability); +const char * LIBUSB_CALL libusb_error_name(int error_code); +int LIBUSB_CALL libusb_setlocale(const char *locale); +const char * LIBUSB_CALL libusb_strerror(int errcode); + +ssize_t LIBUSB_CALL libusb_get_device_list(libusb_context *ctx, + libusb_device ***list); +void LIBUSB_CALL libusb_free_device_list(libusb_device **list, + int unref_devices); +libusb_device * LIBUSB_CALL libusb_ref_device(libusb_device *dev); +void LIBUSB_CALL libusb_unref_device(libusb_device *dev); + +int LIBUSB_CALL libusb_get_device_string(libusb_device *dev, + enum libusb_device_string_type string_type, char *data, int length); +int LIBUSB_CALL libusb_get_configuration(libusb_device_handle *dev, + int *config); +int LIBUSB_CALL libusb_get_device_descriptor(libusb_device *dev, + struct libusb_device_descriptor *desc); +int LIBUSB_CALL libusb_get_active_config_descriptor(libusb_device *dev, + struct libusb_config_descriptor **config); +int LIBUSB_CALL libusb_get_config_descriptor(libusb_device *dev, + uint8_t config_index, struct libusb_config_descriptor **config); +int LIBUSB_CALL libusb_get_config_descriptor_by_value(libusb_device *dev, + uint8_t bConfigurationValue, struct libusb_config_descriptor **config); +void LIBUSB_CALL libusb_free_config_descriptor( + struct libusb_config_descriptor *config); +int LIBUSB_CALL libusb_get_ss_endpoint_companion_descriptor( + libusb_context *ctx, + const struct libusb_endpoint_descriptor *endpoint, + struct libusb_ss_endpoint_companion_descriptor **ep_comp); +void LIBUSB_CALL libusb_free_ss_endpoint_companion_descriptor( + struct libusb_ss_endpoint_companion_descriptor *ep_comp); +int LIBUSB_CALL libusb_get_bos_descriptor(libusb_device_handle *dev_handle, + struct libusb_bos_descriptor **bos); +void LIBUSB_CALL libusb_free_bos_descriptor(struct libusb_bos_descriptor *bos); +int LIBUSB_CALL libusb_get_usb_2_0_extension_descriptor( + libusb_context *ctx, + struct libusb_bos_dev_capability_descriptor *dev_cap, + struct libusb_usb_2_0_extension_descriptor **usb_2_0_extension); +void LIBUSB_CALL libusb_free_usb_2_0_extension_descriptor( + struct libusb_usb_2_0_extension_descriptor *usb_2_0_extension); +int LIBUSB_CALL libusb_get_ss_usb_device_capability_descriptor( + libusb_context *ctx, + struct libusb_bos_dev_capability_descriptor *dev_cap, + struct libusb_ss_usb_device_capability_descriptor **ss_usb_device_cap); +void LIBUSB_CALL libusb_free_ss_usb_device_capability_descriptor( + struct libusb_ss_usb_device_capability_descriptor *ss_usb_device_cap); +int LIBUSB_CALL libusb_get_ssplus_usb_device_capability_descriptor( + libusb_context *ctx, + struct libusb_bos_dev_capability_descriptor *dev_cap, + struct libusb_ssplus_usb_device_capability_descriptor **ssplus_usb_device_cap); +void LIBUSB_CALL libusb_free_ssplus_usb_device_capability_descriptor( + struct libusb_ssplus_usb_device_capability_descriptor *ssplus_usb_device_cap); +int LIBUSB_CALL libusb_get_container_id_descriptor(libusb_context *ctx, + struct libusb_bos_dev_capability_descriptor *dev_cap, + struct libusb_container_id_descriptor **container_id); +void LIBUSB_CALL libusb_free_container_id_descriptor( + struct libusb_container_id_descriptor *container_id); +int LIBUSB_CALL libusb_get_platform_descriptor(libusb_context *ctx, + struct libusb_bos_dev_capability_descriptor *dev_cap, + struct libusb_platform_descriptor **platform_descriptor); +void LIBUSB_CALL libusb_free_platform_descriptor( + struct libusb_platform_descriptor *platform_descriptor); +unsigned long LIBUSB_CALL libusb_get_session_data(libusb_device *dev); +uint8_t LIBUSB_CALL libusb_get_bus_number(libusb_device *dev); +uint8_t LIBUSB_CALL libusb_get_port_number(libusb_device *dev); +int LIBUSB_CALL libusb_get_port_numbers(libusb_device *dev, uint8_t *port_numbers, int port_numbers_len); +LIBUSB_DEPRECATED_FOR(libusb_get_port_numbers) +int LIBUSB_CALL libusb_get_port_path(libusb_context *ctx, libusb_device *dev, uint8_t *path, uint8_t path_length); +libusb_device * LIBUSB_CALL libusb_get_parent(libusb_device *dev); +uint8_t LIBUSB_CALL libusb_get_device_address(libusb_device *dev); +int LIBUSB_CALL libusb_get_device_speed(libusb_device *dev); +int LIBUSB_CALL libusb_get_max_packet_size(libusb_device *dev, + unsigned char endpoint); +int LIBUSB_CALL libusb_get_max_iso_packet_size(libusb_device *dev, + unsigned char endpoint); +int LIBUSB_CALL libusb_get_max_alt_packet_size(libusb_device *dev, + int interface_number, int alternate_setting, unsigned char endpoint); + +int LIBUSB_CALL libusb_get_interface_association_descriptors(libusb_device *dev, + uint8_t config_index, struct libusb_interface_association_descriptor_array **iad_array); +int LIBUSB_CALL libusb_get_active_interface_association_descriptors(libusb_device *dev, + struct libusb_interface_association_descriptor_array **iad_array); +void LIBUSB_CALL libusb_free_interface_association_descriptors( + struct libusb_interface_association_descriptor_array *iad_array); + +int LIBUSB_CALL libusb_wrap_sys_device(libusb_context *ctx, intptr_t sys_dev, libusb_device_handle **dev_handle); +int LIBUSB_CALL libusb_open(libusb_device *dev, libusb_device_handle **dev_handle); +void LIBUSB_CALL libusb_close(libusb_device_handle *dev_handle); +libusb_device * LIBUSB_CALL libusb_get_device(libusb_device_handle *dev_handle); + +int LIBUSB_CALL libusb_set_configuration(libusb_device_handle *dev_handle, + int configuration); +int LIBUSB_CALL libusb_claim_interface(libusb_device_handle *dev_handle, + int interface_number); +int LIBUSB_CALL libusb_release_interface(libusb_device_handle *dev_handle, + int interface_number); + +libusb_device_handle * LIBUSB_CALL libusb_open_device_with_vid_pid( + libusb_context *ctx, uint16_t vendor_id, uint16_t product_id); + +int LIBUSB_CALL libusb_set_interface_alt_setting(libusb_device_handle *dev_handle, + int interface_number, int alternate_setting); +int LIBUSB_CALL libusb_clear_halt(libusb_device_handle *dev_handle, + unsigned char endpoint); +int LIBUSB_CALL libusb_reset_device(libusb_device_handle *dev_handle); + +int LIBUSB_CALL libusb_alloc_streams(libusb_device_handle *dev_handle, + uint32_t num_streams, unsigned char *endpoints, int num_endpoints); +int LIBUSB_CALL libusb_free_streams(libusb_device_handle *dev_handle, + unsigned char *endpoints, int num_endpoints); + +unsigned char * LIBUSB_CALL libusb_dev_mem_alloc(libusb_device_handle *dev_handle, + size_t length); +int LIBUSB_CALL libusb_dev_mem_free(libusb_device_handle *dev_handle, + unsigned char *buffer, size_t length); + +int LIBUSB_CALL libusb_kernel_driver_active(libusb_device_handle *dev_handle, + int interface_number); +int LIBUSB_CALL libusb_detach_kernel_driver(libusb_device_handle *dev_handle, + int interface_number); +int LIBUSB_CALL libusb_attach_kernel_driver(libusb_device_handle *dev_handle, + int interface_number); +int LIBUSB_CALL libusb_set_auto_detach_kernel_driver( + libusb_device_handle *dev_handle, int enable); + +int LIBUSB_CALL libusb_endpoint_supports_raw_io(libusb_device_handle* dev_handle, + uint8_t endpoint); +int LIBUSB_CALL libusb_endpoint_set_raw_io(libusb_device_handle *dev_handle, + uint8_t endpoint, int enable); +int LIBUSB_CALL libusb_get_max_raw_io_transfer_size( + libusb_device_handle *dev_handle, + uint8_t endpoint); + +/* async I/O */ + +/** \ingroup libusb_asyncio + * Get the data section of a control transfer. This convenience function is here + * to remind you that the data does not start until 8 bytes into the actual + * buffer, as the setup packet comes first. + * + * Calling this function only makes sense from a transfer callback function, + * or situations where you have already allocated a suitably sized buffer at + * transfer->buffer. + * + * \param transfer a transfer + * \returns pointer to the first byte of the data section + */ +static inline unsigned char *libusb_control_transfer_get_data( + struct libusb_transfer *transfer) +{ + return transfer->buffer + LIBUSB_CONTROL_SETUP_SIZE; +} + +/** \ingroup libusb_asyncio + * Get the control setup packet of a control transfer. This convenience + * function is here to remind you that the control setup occupies the first + * 8 bytes of the transfer data buffer. + * + * Calling this function only makes sense from a transfer callback function, + * or situations where you have already allocated a suitably sized buffer at + * transfer->buffer. + * + * \param transfer a transfer + * \returns a casted pointer to the start of the transfer data buffer + */ +static inline struct libusb_control_setup *libusb_control_transfer_get_setup( + struct libusb_transfer *transfer) +{ + return (struct libusb_control_setup *)(void *)transfer->buffer; +} + +/** \ingroup libusb_asyncio + * Helper function to populate the setup packet (first 8 bytes of the data + * buffer) for a control transfer. The wIndex, wValue and wLength values should + * be given in host-endian byte order. + * + * \param buffer buffer to output the setup packet into + * This pointer must be aligned to at least 2 bytes boundary. + * \param bmRequestType see the + * \ref libusb_control_setup::bmRequestType "bmRequestType" field of + * \ref libusb_control_setup + * \param bRequest see the + * \ref libusb_control_setup::bRequest "bRequest" field of + * \ref libusb_control_setup + * \param wValue see the + * \ref libusb_control_setup::wValue "wValue" field of + * \ref libusb_control_setup + * \param wIndex see the + * \ref libusb_control_setup::wIndex "wIndex" field of + * \ref libusb_control_setup + * \param wLength see the + * \ref libusb_control_setup::wLength "wLength" field of + * \ref libusb_control_setup + */ +static inline void libusb_fill_control_setup(unsigned char *buffer, + uint8_t bmRequestType, uint8_t bRequest, uint16_t wValue, uint16_t wIndex, + uint16_t wLength) +{ + struct libusb_control_setup *setup = (struct libusb_control_setup *)(void *)buffer; + setup->bmRequestType = bmRequestType; + setup->bRequest = bRequest; + setup->wValue = libusb_cpu_to_le16(wValue); + setup->wIndex = libusb_cpu_to_le16(wIndex); + setup->wLength = libusb_cpu_to_le16(wLength); +} + +struct libusb_transfer * LIBUSB_CALL libusb_alloc_transfer(int iso_packets); +int LIBUSB_CALL libusb_submit_transfer(struct libusb_transfer *transfer); +int LIBUSB_CALL libusb_cancel_transfer(struct libusb_transfer *transfer); +void LIBUSB_CALL libusb_free_transfer(struct libusb_transfer *transfer); +void LIBUSB_CALL libusb_transfer_set_stream_id( + struct libusb_transfer *transfer, uint32_t stream_id); +uint32_t LIBUSB_CALL libusb_transfer_get_stream_id( + struct libusb_transfer *transfer); + +/** \ingroup libusb_asyncio + * Helper function to populate the required \ref libusb_transfer fields + * for a control transfer. + * + * If you pass a transfer buffer to this function, the first 8 bytes will + * be interpreted as a control setup packet, and the wLength field will be + * used to automatically populate the \ref libusb_transfer::length "length" + * field of the transfer. Therefore the recommended approach is: + * -# Allocate a suitably sized data buffer (including space for control setup) + * -# Call libusb_fill_control_setup() + * -# If this is a host-to-device transfer with a data stage, put the data + * in place after the setup packet + * -# Call this function + * -# Call libusb_submit_transfer() + * + * It is also legal to pass a NULL buffer to this function, in which case this + * function will not attempt to populate the length field. Remember that you + * must then populate the buffer and length fields later. + * + * \param transfer the transfer to populate + * \param dev_handle handle of the device that will handle the transfer + * \param buffer data buffer. If provided, this function will interpret the + * first 8 bytes as a setup packet and infer the transfer length from that. + * This pointer must be aligned to at least 2 bytes boundary. + * \param callback callback function to be invoked on transfer completion + * \param user_data user data to pass to callback function + * \param timeout timeout for the transfer in milliseconds + */ +static inline void libusb_fill_control_transfer( + struct libusb_transfer *transfer, libusb_device_handle *dev_handle, + unsigned char *buffer, libusb_transfer_cb_fn callback, void *user_data, + unsigned int timeout) +{ + struct libusb_control_setup *setup = (struct libusb_control_setup *)(void *)buffer; + transfer->dev_handle = dev_handle; + transfer->endpoint = 0; + transfer->type = LIBUSB_TRANSFER_TYPE_CONTROL; + transfer->timeout = timeout; + transfer->buffer = buffer; + if (setup) + transfer->length = (int) (LIBUSB_CONTROL_SETUP_SIZE + + libusb_le16_to_cpu(setup->wLength)); + transfer->user_data = user_data; + transfer->callback = callback; +} + +/** \ingroup libusb_asyncio + * Helper function to populate the required \ref libusb_transfer fields + * for a bulk transfer. + * + * \param transfer the transfer to populate + * \param dev_handle handle of the device that will handle the transfer + * \param endpoint address of the endpoint where this transfer will be sent + * \param buffer data buffer + * \param length length of data buffer + * \param callback callback function to be invoked on transfer completion + * \param user_data user data to pass to callback function + * \param timeout timeout for the transfer in milliseconds + */ +static inline void libusb_fill_bulk_transfer(struct libusb_transfer *transfer, + libusb_device_handle *dev_handle, unsigned char endpoint, + unsigned char *buffer, int length, libusb_transfer_cb_fn callback, + void *user_data, unsigned int timeout) +{ + transfer->dev_handle = dev_handle; + transfer->endpoint = endpoint; + transfer->type = LIBUSB_TRANSFER_TYPE_BULK; + transfer->timeout = timeout; + transfer->buffer = buffer; + transfer->length = length; + transfer->user_data = user_data; + transfer->callback = callback; +} + +/** \ingroup libusb_asyncio + * Helper function to populate the required \ref libusb_transfer fields + * for a bulk transfer using bulk streams. + * + * Since version 1.0.19, \ref LIBUSB_API_VERSION >= 0x01000103 + * + * \param transfer the transfer to populate + * \param dev_handle handle of the device that will handle the transfer + * \param endpoint address of the endpoint where this transfer will be sent + * \param stream_id bulk stream id for this transfer + * \param buffer data buffer + * \param length length of data buffer + * \param callback callback function to be invoked on transfer completion + * \param user_data user data to pass to callback function + * \param timeout timeout for the transfer in milliseconds + */ +static inline void libusb_fill_bulk_stream_transfer( + struct libusb_transfer *transfer, libusb_device_handle *dev_handle, + unsigned char endpoint, uint32_t stream_id, + unsigned char *buffer, int length, libusb_transfer_cb_fn callback, + void *user_data, unsigned int timeout) +{ + libusb_fill_bulk_transfer(transfer, dev_handle, endpoint, buffer, + length, callback, user_data, timeout); + transfer->type = LIBUSB_TRANSFER_TYPE_BULK_STREAM; + libusb_transfer_set_stream_id(transfer, stream_id); +} + +/** \ingroup libusb_asyncio + * Helper function to populate the required \ref libusb_transfer fields + * for an interrupt transfer. + * + * \param transfer the transfer to populate + * \param dev_handle handle of the device that will handle the transfer + * \param endpoint address of the endpoint where this transfer will be sent + * \param buffer data buffer + * \param length length of data buffer + * \param callback callback function to be invoked on transfer completion + * \param user_data user data to pass to callback function + * \param timeout timeout for the transfer in milliseconds + */ +static inline void libusb_fill_interrupt_transfer( + struct libusb_transfer *transfer, libusb_device_handle *dev_handle, + unsigned char endpoint, unsigned char *buffer, int length, + libusb_transfer_cb_fn callback, void *user_data, unsigned int timeout) +{ + transfer->dev_handle = dev_handle; + transfer->endpoint = endpoint; + transfer->type = LIBUSB_TRANSFER_TYPE_INTERRUPT; + transfer->timeout = timeout; + transfer->buffer = buffer; + transfer->length = length; + transfer->user_data = user_data; + transfer->callback = callback; +} + +/** \ingroup libusb_asyncio + * Helper function to populate the required \ref libusb_transfer fields + * for an isochronous transfer. + * + * \param transfer the transfer to populate + * \param dev_handle handle of the device that will handle the transfer + * \param endpoint address of the endpoint where this transfer will be sent + * \param buffer data buffer + * \param length length of data buffer + * \param num_iso_packets the number of isochronous packets + * \param callback callback function to be invoked on transfer completion + * \param user_data user data to pass to callback function + * \param timeout timeout for the transfer in milliseconds + */ +static inline void libusb_fill_iso_transfer(struct libusb_transfer *transfer, + libusb_device_handle *dev_handle, unsigned char endpoint, + unsigned char *buffer, int length, int num_iso_packets, + libusb_transfer_cb_fn callback, void *user_data, unsigned int timeout) +{ + transfer->dev_handle = dev_handle; + transfer->endpoint = endpoint; + transfer->type = LIBUSB_TRANSFER_TYPE_ISOCHRONOUS; + transfer->timeout = timeout; + transfer->buffer = buffer; + transfer->length = length; + transfer->num_iso_packets = num_iso_packets; + transfer->user_data = user_data; + transfer->callback = callback; +} + +/** \ingroup libusb_asyncio + * Convenience function to set the length of all packets in an isochronous + * transfer, based on the num_iso_packets field in the transfer structure. + * + * \param transfer a transfer + * \param length the length to set in each isochronous packet descriptor + * \see libusb_get_max_packet_size() + */ +static inline void libusb_set_iso_packet_lengths( + struct libusb_transfer *transfer, unsigned int length) +{ + int i; + + for (i = 0; i < transfer->num_iso_packets; i++) + transfer->iso_packet_desc[i].length = length; +} + +/** \ingroup libusb_asyncio + * Convenience function to locate the position of an isochronous packet + * within the buffer of an isochronous transfer. + * + * This is a thorough function which loops through all preceding packets, + * accumulating their lengths to find the position of the specified packet. + * Typically you will assign equal lengths to each packet in the transfer, + * and hence the above method is sub-optimal. You may wish to use + * libusb_get_iso_packet_buffer_simple() instead. + * + * \param transfer a transfer + * \param packet the packet to return the address of + * \returns the base address of the packet buffer inside the transfer buffer, + * or NULL if the packet does not exist. + * \see libusb_get_iso_packet_buffer_simple() + */ +static inline unsigned char *libusb_get_iso_packet_buffer( + struct libusb_transfer *transfer, unsigned int packet) +{ + int i; + size_t offset = 0; + int _packet; + + /* oops..slight bug in the API. packet is an unsigned int, but we use + * signed integers almost everywhere else. range-check and convert to + * signed to avoid compiler warnings. FIXME for libusb-2. */ + if (packet > INT_MAX) + return NULL; + _packet = (int) packet; + + if (_packet >= transfer->num_iso_packets) + return NULL; + + for (i = 0; i < _packet; i++) + offset += transfer->iso_packet_desc[i].length; + + return transfer->buffer + offset; +} + +/** \ingroup libusb_asyncio + * Convenience function to locate the position of an isochronous packet + * within the buffer of an isochronous transfer, for transfers where each + * packet is of identical size. + * + * This function relies on the assumption that every packet within the transfer + * is of identical size to the first packet. Calculating the location of + * the packet buffer is then just a simple calculation: + * buffer + (packet_size * packet) + * + * Do not use this function on transfers other than those that have identical + * packet lengths for each packet. + * + * \param transfer a transfer + * \param packet the packet to return the address of + * \returns the base address of the packet buffer inside the transfer buffer, + * or NULL if the packet does not exist. + * \see libusb_get_iso_packet_buffer() + */ +static inline unsigned char *libusb_get_iso_packet_buffer_simple( + struct libusb_transfer *transfer, unsigned int packet) +{ + int _packet; + + /* oops..slight bug in the API. packet is an unsigned int, but we use + * signed integers almost everywhere else. range-check and convert to + * signed to avoid compiler warnings. FIXME for libusb-2. */ + if (packet > INT_MAX) + return NULL; + _packet = (int) packet; + + if (_packet >= transfer->num_iso_packets) + return NULL; + + return transfer->buffer + ((int) transfer->iso_packet_desc[0].length * _packet); +} + +/* sync I/O */ + +int LIBUSB_CALL libusb_control_transfer(libusb_device_handle *dev_handle, + uint8_t bmRequestType, uint8_t bRequest, uint16_t wValue, uint16_t wIndex, + unsigned char *data, uint16_t wLength, unsigned int timeout); + +int LIBUSB_CALL libusb_bulk_transfer(libusb_device_handle *dev_handle, + unsigned char endpoint, unsigned char *data, int length, + int *transferred, unsigned int timeout); + +int LIBUSB_CALL libusb_interrupt_transfer(libusb_device_handle *dev_handle, + unsigned char endpoint, unsigned char *data, int length, + int *transferred, unsigned int timeout); + +/** \ingroup libusb_desc + * Retrieve a descriptor from the default control pipe. + * This is a convenience function which formulates the appropriate control + * message to retrieve the descriptor. + * + * \param dev_handle a device handle + * \param desc_type the descriptor type, see \ref libusb_descriptor_type + * \param desc_index the index of the descriptor to retrieve + * \param data output buffer for descriptor + * \param length size of data buffer + * \returns number of bytes returned in data, or LIBUSB_ERROR code on failure + */ +static inline int libusb_get_descriptor(libusb_device_handle *dev_handle, + uint8_t desc_type, uint8_t desc_index, unsigned char *data, int length) +{ + return libusb_control_transfer(dev_handle, LIBUSB_ENDPOINT_IN, + LIBUSB_REQUEST_GET_DESCRIPTOR, (uint16_t) ((desc_type << 8) | desc_index), + 0, data, (uint16_t) length, 1000); +} + +/** \ingroup libusb_desc + * Retrieve a descriptor from a device. + * This is a convenience function which formulates the appropriate control + * message to retrieve the descriptor. The string returned is Unicode, as + * detailed in the USB specifications. + * + * \param dev_handle a device handle + * \param desc_index the index of the descriptor to retrieve + * \param langid the language ID for the string descriptor + * \param data output buffer for descriptor + * \param length size of data buffer + * \returns number of bytes returned in data, or LIBUSB_ERROR code on failure + * \see libusb_get_string_descriptor_ascii() + */ +static inline int libusb_get_string_descriptor(libusb_device_handle *dev_handle, + uint8_t desc_index, uint16_t langid, unsigned char *data, int length) +{ + return libusb_control_transfer(dev_handle, LIBUSB_ENDPOINT_IN, + LIBUSB_REQUEST_GET_DESCRIPTOR, (uint16_t)((LIBUSB_DT_STRING << 8) | desc_index), + langid, data, (uint16_t) length, 1000); +} + +int LIBUSB_CALL libusb_get_string_descriptor_ascii(libusb_device_handle *dev_handle, + uint8_t desc_index, unsigned char *data, int length); + +/* polling and timeouts */ + +int LIBUSB_CALL libusb_try_lock_events(libusb_context *ctx); +void LIBUSB_CALL libusb_lock_events(libusb_context *ctx); +void LIBUSB_CALL libusb_unlock_events(libusb_context *ctx); +int LIBUSB_CALL libusb_event_handling_ok(libusb_context *ctx); +int LIBUSB_CALL libusb_event_handler_active(libusb_context *ctx); +void LIBUSB_CALL libusb_interrupt_event_handler(libusb_context *ctx); +void LIBUSB_CALL libusb_lock_event_waiters(libusb_context *ctx); +void LIBUSB_CALL libusb_unlock_event_waiters(libusb_context *ctx); +int LIBUSB_CALL libusb_wait_for_event(libusb_context *ctx, struct timeval *tv); + +int LIBUSB_CALL libusb_handle_events_timeout(libusb_context *ctx, + struct timeval *tv); +int LIBUSB_CALL libusb_handle_events_timeout_completed(libusb_context *ctx, + struct timeval *tv, int *completed); +int LIBUSB_CALL libusb_handle_events(libusb_context *ctx); +int LIBUSB_CALL libusb_handle_events_completed(libusb_context *ctx, int *completed); +int LIBUSB_CALL libusb_handle_events_locked(libusb_context *ctx, + struct timeval *tv); +int LIBUSB_CALL libusb_pollfds_handle_timeouts(libusb_context *ctx); +int LIBUSB_CALL libusb_get_next_timeout(libusb_context *ctx, + struct timeval *tv); + +/** \ingroup libusb_poll + * File descriptor for polling + */ +struct libusb_pollfd { + /** Numeric file descriptor */ + int fd; + + /** Event flags to poll for from . POLLIN indicates that you + * should monitor this file descriptor for becoming ready to read from, + * and POLLOUT indicates that you should monitor this file descriptor for + * nonblocking write readiness. */ + short events; +}; + +/** \ingroup libusb_poll + * Callback function, invoked when a new file descriptor should be added + * to the set of file descriptors monitored for events. + * \param fd the new file descriptor + * \param events events to monitor for, see \ref libusb_pollfd for a + * description + * \param user_data User data pointer specified in + * libusb_set_pollfd_notifiers() call + * \see libusb_set_pollfd_notifiers() + */ +typedef void (LIBUSB_CALL *libusb_pollfd_added_cb)(int fd, short events, + void *user_data); + +/** \ingroup libusb_poll + * Callback function, invoked when a file descriptor should be removed from + * the set of file descriptors being monitored for events. After returning + * from this callback, do not use that file descriptor again. + * \param fd the file descriptor to stop monitoring + * \param user_data User data pointer specified in + * libusb_set_pollfd_notifiers() call + * \see libusb_set_pollfd_notifiers() + */ +typedef void (LIBUSB_CALL *libusb_pollfd_removed_cb)(int fd, void *user_data); + +const struct libusb_pollfd ** LIBUSB_CALL libusb_get_pollfds( + libusb_context *ctx); +void LIBUSB_CALL libusb_free_pollfds(const struct libusb_pollfd **pollfds); +void LIBUSB_CALL libusb_set_pollfd_notifiers(libusb_context *ctx, + libusb_pollfd_added_cb added_cb, libusb_pollfd_removed_cb removed_cb, + void *user_data); + +/** \ingroup libusb_hotplug + * Callback handle. + * + * Callbacks handles are generated by libusb_hotplug_register_callback() + * and can be used to deregister callbacks. Callback handles are unique + * per libusb_context and it is safe to call libusb_hotplug_deregister_callback() + * on an already deregistered callback. + * + * Since version 1.0.16, \ref LIBUSB_API_VERSION >= 0x01000102 + * + * For more information, see \ref libusb_hotplug. + */ +typedef int libusb_hotplug_callback_handle; + +/** \ingroup libusb_hotplug + * + * Since version 1.0.16, \ref LIBUSB_API_VERSION >= 0x01000102 + * + * Hotplug events */ +typedef enum { + /** A device has been plugged in and is ready to use */ + LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED = (1 << 0), + + /** A device has left and is no longer available. + * It is the user's responsibility to call libusb_close on any handle associated with a disconnected device. + * It is safe to call libusb_get_device_descriptor on a device that has left */ + LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT = (1 << 1) +} libusb_hotplug_event; + +/** \ingroup libusb_hotplug + * + * Since version 1.0.16, \ref LIBUSB_API_VERSION >= 0x01000102 + * + * Hotplug flags */ +typedef enum { + /** Arm the callback and fire it for all matching currently attached devices. */ + LIBUSB_HOTPLUG_ENUMERATE = (1 << 0) +} libusb_hotplug_flag; + +/** \ingroup libusb_hotplug + * Convenience macro when not using any flags */ +#define LIBUSB_HOTPLUG_NO_FLAGS 0 + +/** \ingroup libusb_hotplug + * Wildcard matching for hotplug events */ +#define LIBUSB_HOTPLUG_MATCH_ANY -1 + +/** \ingroup libusb_hotplug + * Hotplug callback function type. When requesting hotplug event notifications, + * you pass a pointer to a callback function of this type. + * + * This callback may be called by an internal event thread and as such it is + * recommended the callback do minimal processing before returning. + * + * libusb will call this function later, when a matching event had happened on + * a matching device. See \ref libusb_hotplug for more information. + * + * It is safe to call either libusb_hotplug_register_callback() or + * libusb_hotplug_deregister_callback() from within a callback function. + * + * Since version 1.0.16, \ref LIBUSB_API_VERSION >= 0x01000102 + * + * \param ctx context of this notification + * \param device libusb_device this event occurred on + * \param event event that occurred + * \param user_data user data provided when this callback was registered + * \returns bool whether this callback is finished processing events. + * returning 1 will cause this callback to be deregistered + */ +typedef int (LIBUSB_CALL *libusb_hotplug_callback_fn)(libusb_context *ctx, + libusb_device *device, libusb_hotplug_event event, void *user_data); + +/** \ingroup libusb_hotplug + * Register a hotplug callback function + * + * Register a callback with the libusb_context. The callback will fire + * when a matching event occurs on a matching device. The callback is + * armed until either it is deregistered with libusb_hotplug_deregister_callback() + * or the supplied callback returns 1 to indicate it is finished processing events. + * + * If the \ref LIBUSB_HOTPLUG_ENUMERATE is passed the callback will be + * called with a \ref LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED for all devices + * already plugged into the machine. Note that libusb modifies its internal + * device list from a separate thread, while calling hotplug callbacks from + * libusb_handle_events(), so it is possible for a device to already be present + * on, or removed from, its internal device list, while the hotplug callbacks + * still need to be dispatched. This means that when using \ref + * LIBUSB_HOTPLUG_ENUMERATE, your callback may be called twice for the arrival + * of the same device, once from libusb_hotplug_register_callback() and once + * from libusb_handle_events(); and/or your callback may be called for the + * removal of a device for which an arrived call was never made. + * + * Since version 1.0.16, \ref LIBUSB_API_VERSION >= 0x01000102 + * + * \param[in] ctx context to register this callback with + * \param[in] events bitwise or of hotplug events that will trigger this callback. + * See \ref libusb_hotplug_event + * \param[in] flags bitwise or of hotplug flags that affect registration. + * See \ref libusb_hotplug_flag + * \param[in] vendor_id the vendor id to match or \ref LIBUSB_HOTPLUG_MATCH_ANY + * \param[in] product_id the product id to match or \ref LIBUSB_HOTPLUG_MATCH_ANY + * \param[in] dev_class the device class to match or \ref LIBUSB_HOTPLUG_MATCH_ANY + * \param[in] cb_fn the function to be invoked on a matching event/device + * \param[in] user_data user data to pass to the callback function + * \param[out] callback_handle pointer to store the handle of the allocated callback (can be NULL) + * \returns \ref LIBUSB_SUCCESS on success LIBUSB_ERROR code on failure + */ +int LIBUSB_CALL libusb_hotplug_register_callback(libusb_context *ctx, + int events, int flags, + int vendor_id, int product_id, int dev_class, + libusb_hotplug_callback_fn cb_fn, void *user_data, + libusb_hotplug_callback_handle *callback_handle); + +/** \ingroup libusb_hotplug + * Deregisters a hotplug callback. + * + * Deregister a callback from a libusb_context. This function is safe to call from within + * a hotplug callback. + * + * Since version 1.0.16, \ref LIBUSB_API_VERSION >= 0x01000102 + * + * \param[in] ctx context this callback is registered with + * \param[in] callback_handle the handle of the callback to deregister + */ +void LIBUSB_CALL libusb_hotplug_deregister_callback(libusb_context *ctx, + libusb_hotplug_callback_handle callback_handle); + +/** \ingroup libusb_hotplug + * Gets the user_data associated with a hotplug callback. + * + * Since version v1.0.24 \ref LIBUSB_API_VERSION >= 0x01000108 + * + * \param[in] ctx context this callback is registered with + * \param[in] callback_handle the handle of the callback to get the user_data of + */ +void * LIBUSB_CALL libusb_hotplug_get_user_data(libusb_context *ctx, + libusb_hotplug_callback_handle callback_handle); + +int LIBUSB_CALLV libusb_set_option(libusb_context *ctx, enum libusb_option option, ...); + +#ifdef _MSC_VER +#pragma warning(pop) +#endif + +#if defined(__cplusplus) +} +#endif + +#endif diff --git a/usbaudio/src/main/cpp/external/libusb/libusb/libusbi.h b/usbaudio/src/main/cpp/external/libusb/libusb/libusbi.h new file mode 100644 index 000000000..0fcbdd200 --- /dev/null +++ b/usbaudio/src/main/cpp/external/libusb/libusb/libusbi.h @@ -0,0 +1,1599 @@ +/* + * Internal header for libusb + * Copyright © 2007-2009 Daniel Drake + * Copyright © 2001 Johannes Erdfelt + * Copyright © 2019 Nathan Hjelm + * Copyright © 2019-2020 Google LLC. All rights reserved. + * Copyright © 2020 Chris Dickens + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef LIBUSBI_H +#define LIBUSBI_H + +#include + +#include +#include +#include +#include +#include +#ifdef HAVE_SYS_TIME_H +#include +#endif + +#include "libusb.h" + +/* Not all C standard library headers define static_assert in assert.h + * Additionally, Visual Studio treats static_assert as a keyword. + */ +#if !defined(__cplusplus) && !defined(static_assert) && !defined(_MSC_VER) +#define static_assert(cond, msg) _Static_assert(cond, msg) +#endif + +#ifdef NDEBUG +#define ASSERT_EQ(expression, value) (void)expression +#define ASSERT_NE(expression, value) (void)expression +#else +#define ASSERT_EQ(expression, value) assert(expression == value) +#define ASSERT_NE(expression, value) assert(expression != value) +#endif + +#define container_of(ptr, type, member) \ + ((type *)((uintptr_t)(ptr) - (uintptr_t)offsetof(type, member))) + +#ifndef ARRAYSIZE +#define ARRAYSIZE(array) (sizeof(array) / sizeof(array[0])) +#endif + +#ifndef CLAMP +#define CLAMP(val, min, max) \ + ((val) < (min) ? (min) : ((val) > (max) ? (max) : (val))) +#endif + +#ifndef MIN +#define MIN(a, b) ((a) < (b) ? (a) : (b)) +#endif + +#ifndef MAX +#define MAX(a, b) ((a) > (b) ? (a) : (b)) +#endif + +/* The following is used to silence warnings for unused variables */ +#if defined(UNREFERENCED_PARAMETER) && !defined(__GNUC__) +#define UNUSED(var) UNREFERENCED_PARAMETER(var) +#else +#define UNUSED(var) do { (void)(var); } while(0) +#endif + +/* Macro to align a value up to the next multiple of the size of a pointer */ +#define PTR_ALIGN(v) \ + (((v) + (sizeof(void *) - 1)) & ~(sizeof(void *) - 1)) + +/* Atomic operations + * + * Useful for reference counting or when accessing a value without a lock + * + * The following atomic operations are defined: + * usbi_atomic_load() - Atomically read a variable's value + * usbi_atomic_store() - Atomically write a new value value to a variable + * usbi_atomic_inc() - Atomically increment a variable's value and return the new value + * usbi_atomic_dec() - Atomically decrement a variable's value and return the new value + * + * All of these operations are ordered with each other, thus the effects of + * any one operation is guaranteed to be seen by any other operation. + */ +#ifdef _MSC_VER +typedef volatile LONG usbi_atomic_t; +#define usbi_atomic_load(a) (*(a)) +#define usbi_atomic_store(a, v) (*(a)) = (v) +#define usbi_atomic_inc(a) InterlockedIncrement((a)) +#define usbi_atomic_dec(a) InterlockedDecrement((a)) +#else +#if defined(__HAIKU__) && defined(__GNUC__) && !defined(__clang__) +/* The Haiku port of libusb has some C++ files and GCC does not define + * anything in stdatomic.h when compiled in C++11 (only in C++23). + * This appears to be a bug in gcc's stdatomic.h, and should be fixed either + * in gcc or in Haiku. Until then, use the gcc builtins. */ +typedef long usbi_atomic_t; +#define usbi_atomic_load(a) __atomic_load_n((a), __ATOMIC_SEQ_CST) +#define usbi_atomic_store(a, v) __atomic_store_n((a), (v), __ATOMIC_SEQ_CST) +#define usbi_atomic_inc(a) __atomic_add_fetch((a), 1, __ATOMIC_SEQ_CST) +#define usbi_atomic_dec(a) __atomic_sub_fetch((a), 1, __ATOMIC_SEQ_CST) +#else +#include +typedef atomic_long usbi_atomic_t; +#define usbi_atomic_load(a) atomic_load((a)) +#define usbi_atomic_store(a, v) atomic_store((a), (v)) +#define usbi_atomic_inc(a) (atomic_fetch_add((a), 1) + 1) +#define usbi_atomic_dec(a) (atomic_fetch_add((a), -1) - 1) +#endif +#endif + +/* Internal abstractions for event handling and thread synchronization */ +#if defined(PLATFORM_POSIX) +#include "os/events_posix.h" +#include "os/threads_posix.h" +#elif defined(PLATFORM_WINDOWS) +#include "os/events_windows.h" +#include "os/threads_windows.h" +#endif + +/* Inside the libusb code, mark all public functions as follows: + * return_type API_EXPORTED function_name(params) { ... } + * But if the function returns a pointer, mark it as follows: + * DEFAULT_VISIBILITY return_type * LIBUSB_CALL function_name(params) { ... } + * In the libusb public header, mark all declarations as: + * return_type LIBUSB_CALL function_name(params); + */ +#define API_EXPORTED LIBUSB_CALL DEFAULT_VISIBILITY +#define API_EXPORTEDV LIBUSB_CALLV DEFAULT_VISIBILITY + +#ifdef __cplusplus +extern "C" { +#endif + +#define USB_MAXENDPOINTS 32 +#define USB_MAXINTERFACES 32 +#define USB_MAXCONFIG 8 + +/* Backend specific capabilities */ +#define USBI_CAP_HAS_HID_ACCESS 0x00010000 +#define USBI_CAP_SUPPORTS_DETACH_KERNEL_DRIVER 0x00020000 + +/* Maximum number of bytes in a log line */ +#define USBI_MAX_LOG_LEN 1024 +/* Terminator for log lines */ +#define USBI_LOG_LINE_END "\n" + +struct list_head { + struct list_head *prev, *next; +}; + +/* Get an entry from the list + * ptr - the address of this list_head element in "type" + * type - the data type that contains "member" + * member - the list_head element in "type" + */ +#define list_entry(ptr, type, member) \ + container_of(ptr, type, member) + +#define list_first_entry(ptr, type, member) \ + list_entry((ptr)->next, type, member) + +#define list_next_entry(ptr, type, member) \ + list_entry((ptr)->member.next, type, member) + +/* Get each entry from a list + * pos - A structure pointer has a "member" element + * head - list head + * member - the list_head element in "pos" + * type - the type of the first parameter + */ +#define list_for_each_entry(pos, head, member, type) \ + for (pos = list_first_entry(head, type, member); \ + &pos->member != (head); \ + pos = list_next_entry(pos, type, member)) + +#define list_for_each_entry_safe(pos, n, head, member, type) \ + for (pos = list_first_entry(head, type, member), \ + n = list_next_entry(pos, type, member); \ + &pos->member != (head); \ + pos = n, n = list_next_entry(n, type, member)) + +/* Helper macros to iterate over a list. The structure pointed + * to by "pos" must have a list_head member named "list". + */ +#define for_each_helper(pos, head, type) \ + list_for_each_entry(pos, head, list, type) + +#define for_each_safe_helper(pos, n, head, type) \ + list_for_each_entry_safe(pos, n, head, list, type) + +#define list_empty(entry) ((entry)->next == (entry)) + +static inline void list_init(struct list_head *entry) +{ + entry->prev = entry->next = entry; +} + +static inline void list_add(struct list_head *entry, struct list_head *head) +{ + entry->next = head->next; + entry->prev = head; + + head->next->prev = entry; + head->next = entry; +} + +static inline void list_add_tail(struct list_head *entry, + struct list_head *head) +{ + entry->next = head; + entry->prev = head->prev; + + head->prev->next = entry; + head->prev = entry; +} + +static inline void list_del(struct list_head *entry) +{ + entry->next->prev = entry->prev; + entry->prev->next = entry->next; + entry->next = entry->prev = NULL; +} + +static inline void list_cut(struct list_head *list, struct list_head *head) +{ + if (list_empty(head)) { + list_init(list); + return; + } + + list->next = head->next; + list->next->prev = list; + list->prev = head->prev; + list->prev->next = list; + + list_init(head); +} + +static inline void list_splice_front(struct list_head *list, struct list_head *head) +{ + list->next->prev = head; + list->prev->next = head->next; + head->next->prev = list->prev; + head->next = list->next; +} + +static inline void *usbi_reallocf(void *ptr, size_t size) +{ + void *ret = realloc(ptr, size); + + if (!ret) + free(ptr); + return ret; +} + +#if !defined(USEC_PER_SEC) +#define USEC_PER_SEC 1000000L +#endif + +#if !defined(NSEC_PER_SEC) +#define NSEC_PER_SEC 1000000000L +#endif + +#define TIMEVAL_IS_VALID(tv) \ + ((tv)->tv_sec >= 0 && \ + (tv)->tv_usec >= 0 && (tv)->tv_usec < USEC_PER_SEC) + +#define TIMESPEC_IS_SET(ts) ((ts)->tv_sec || (ts)->tv_nsec) +#define TIMESPEC_CLEAR(ts) (ts)->tv_sec = (ts)->tv_nsec = 0 +#define TIMESPEC_CMP(a, b, CMP) \ + (((a)->tv_sec == (b)->tv_sec) \ + ? ((a)->tv_nsec CMP (b)->tv_nsec) \ + : ((a)->tv_sec CMP (b)->tv_sec)) +#define TIMESPEC_SUB(a, b, result) \ + do { \ + (result)->tv_sec = (a)->tv_sec - (b)->tv_sec; \ + (result)->tv_nsec = (a)->tv_nsec - (b)->tv_nsec; \ + if ((result)->tv_nsec < 0L) { \ + --(result)->tv_sec; \ + (result)->tv_nsec += NSEC_PER_SEC; \ + } \ + } while (0) + +#if defined(PLATFORM_WINDOWS) +#define TIMEVAL_TV_SEC_TYPE long +#else +#define TIMEVAL_TV_SEC_TYPE time_t +#endif + +/* Some platforms don't have this define */ +#ifndef TIMESPEC_TO_TIMEVAL +#define TIMESPEC_TO_TIMEVAL(tv, ts) \ + do { \ + (tv)->tv_sec = (TIMEVAL_TV_SEC_TYPE) (ts)->tv_sec; \ + (tv)->tv_usec = (ts)->tv_nsec / 1000L; \ + } while (0) +#endif + +#ifdef ENABLE_LOGGING + +#if defined(_MSC_VER) && (_MSC_VER < 1900) +#include +#define snprintf usbi_snprintf +#define vsnprintf usbi_vsnprintf +int usbi_snprintf(char *dst, size_t size, const char *format, ...); +int usbi_vsnprintf(char *dst, size_t size, const char *format, va_list args); +#define LIBUSB_PRINTF_WIN32 +#endif /* defined(_MSC_VER) && (_MSC_VER < 1900) */ + +void usbi_log(struct libusb_context *ctx, enum libusb_log_level level, + const char *function, const char *format, ...) PRINTF_FORMAT(4, 5); + +#define _usbi_log(ctx, level, ...) usbi_log(ctx, level, __func__, __VA_ARGS__) + +#define usbi_err(ctx, ...) _usbi_log(ctx, LIBUSB_LOG_LEVEL_ERROR, __VA_ARGS__) +#define usbi_warn(ctx, ...) _usbi_log(ctx, LIBUSB_LOG_LEVEL_WARNING, __VA_ARGS__) +#define usbi_info(ctx, ...) _usbi_log(ctx, LIBUSB_LOG_LEVEL_INFO, __VA_ARGS__) +#define usbi_dbg(ctx ,...) _usbi_log(ctx, LIBUSB_LOG_LEVEL_DEBUG, __VA_ARGS__) + +#else /* ENABLE_LOGGING */ + +#define usbi_err(ctx, ...) do { (void)(ctx); } while(0) +#define usbi_warn(ctx, ...) do { (void)(ctx); } while(0) +#define usbi_info(ctx, ...) do { (void)(ctx); } while(0) +#define usbi_dbg(ctx, ...) do { (void)(ctx); } while(0) + +#endif /* ENABLE_LOGGING */ + +#define DEVICE_CTX(dev) ((dev)->ctx) +#define HANDLE_CTX(handle) ((handle) ? DEVICE_CTX((handle)->dev) : NULL) +#define ITRANSFER_CTX(itransfer) \ + ((itransfer)->dev ? DEVICE_CTX((itransfer)->dev) : NULL) +#define TRANSFER_CTX(transfer) \ + (ITRANSFER_CTX(LIBUSB_TRANSFER_TO_USBI_TRANSFER(transfer))) + +#define IS_EPIN(ep) (0 != ((ep) & LIBUSB_ENDPOINT_IN)) +#define IS_EPOUT(ep) (!IS_EPIN(ep)) +#define IS_XFERIN(xfer) (0 != ((xfer)->endpoint & LIBUSB_ENDPOINT_IN)) +#define IS_XFEROUT(xfer) (!IS_XFERIN(xfer)) + +struct libusb_context { +#if defined(ENABLE_LOGGING) && !defined(ENABLE_DEBUG_LOGGING) + enum libusb_log_level debug; + int debug_fixed; + libusb_log_cb log_handler; +#endif + + /* used for signalling occurrence of an internal event. */ + usbi_event_t event; + +#ifdef HAVE_OS_TIMER + /* used for timeout handling, if supported by OS. + * this timer is maintained to trigger on the next pending timeout */ + usbi_timer_t timer; +#endif + + usbi_mutex_t usb_devs_lock; + struct list_head usb_devs; + + /* A list of open handles. Backends are free to traverse this if required. + */ + usbi_mutex_t open_devs_lock; + struct list_head open_devs; + + /* A list of registered hotplug callbacks */ + usbi_mutex_t hotplug_cbs_lock; + struct list_head hotplug_cbs; + libusb_hotplug_callback_handle next_hotplug_cb_handle; + + /* A flag to indicate that the context is ready for hotplug notifications */ + usbi_atomic_t hotplug_ready; + + /* Note paths taking both this and usbi_transfer->lock must always + * take this lock first */ + usbi_mutex_t flying_transfers_lock; + /* this is a list of in-flight transfer handles, sorted by timeout + * expiration. URBs to timeout the soonest are placed at the beginning of + * the list, URBs that will time out later are placed after, and urbs with + * infinite timeout are always placed at the very end. */ + struct list_head flying_transfers; + +#if !defined(PLATFORM_WINDOWS) + /* user callbacks for pollfd changes */ + libusb_pollfd_added_cb fd_added_cb; + libusb_pollfd_removed_cb fd_removed_cb; + void *fd_cb_user_data; +#endif + + /* ensures that only one thread is handling events at any one time */ + usbi_mutex_t events_lock; + + /* used to see if there is an active thread doing event handling */ + int event_handler_active; + + /* A thread-local storage key to track which thread is performing event + * handling */ + usbi_tls_key_t event_handling_key; + + /* used to wait for event completion in threads other than the one that is + * event handling */ + usbi_mutex_t event_waiters_lock; + usbi_cond_t event_waiters_cond; + + /* A lock to protect internal context event data. */ + usbi_mutex_t event_data_lock; + + /* A bitmask of flags that are set to indicate specific events that need to + * be handled. Protected by event_data_lock. */ + unsigned int event_flags; + + /* A counter that is set when we want to interrupt and prevent event handling, + * in order to safely close a device. Protected by event_data_lock. */ + unsigned int device_close; + + /* A list of currently active event sources. Protected by event_data_lock. */ + struct list_head event_sources; + + /* A list of event sources that have been removed since the last time + * event sources were waited on. Protected by event_data_lock. */ + struct list_head removed_event_sources; + + /* A pointer and count to platform-specific data used for monitoring event + * sources. Only accessed during event handling. */ + void *event_data; + unsigned int event_data_cnt; + + /* A list of pending hotplug messages. Protected by event_data_lock. */ + struct list_head hotplug_msgs; + + /* A list of pending completed transfers. Protected by event_data_lock. */ + struct list_head completed_transfers; + + struct list_head list; +}; + +extern struct libusb_context *usbi_default_context; +extern struct libusb_context *usbi_fallback_context; + +extern usbi_mutex_static_t active_contexts_lock; +extern struct list_head active_contexts_list; + +static inline struct libusb_context *usbi_get_context(struct libusb_context *ctx) +{ + static int warned = 0; + + if (!ctx) { + ctx = usbi_default_context; + } + if (!ctx) { + ctx = usbi_fallback_context; + if (ctx && warned == 0) { + usbi_err(ctx, "API misuse! Using non-default context as implicit default."); + warned = 1; + } + } + return ctx; +} + +enum usbi_event_flags { + /* The list of event sources has been modified */ + USBI_EVENT_EVENT_SOURCES_MODIFIED = 1U << 0, + + /* The user has interrupted the event handler */ + USBI_EVENT_USER_INTERRUPT = 1U << 1, + + /* A hotplug callback deregistration is pending */ + USBI_EVENT_HOTPLUG_CB_DEREGISTERED = 1U << 2, + + /* One or more hotplug messages are pending */ + USBI_EVENT_HOTPLUG_MSG_PENDING = 1U << 3, + + /* One or more completed transfers are pending */ + USBI_EVENT_TRANSFER_COMPLETED = 1U << 4, + + /* A device is in the process of being closed */ + USBI_EVENT_DEVICE_CLOSE = 1U << 5, +}; + +/* Macros for managing event handling state */ +static inline int usbi_handling_events(struct libusb_context *ctx) +{ + return usbi_tls_key_get(ctx->event_handling_key) != NULL; +} + +static inline void usbi_start_event_handling(struct libusb_context *ctx) +{ + usbi_tls_key_set(ctx->event_handling_key, ctx); +} + +static inline void usbi_end_event_handling(struct libusb_context *ctx) +{ + usbi_tls_key_set(ctx->event_handling_key, NULL); +} + +struct libusb_device { + usbi_atomic_t refcnt; + + struct libusb_context *ctx; + struct libusb_device *parent_dev; + + uint8_t bus_number; + uint8_t port_number; + uint8_t device_address; + enum libusb_speed speed; + + struct list_head list; + unsigned long session_data; + + struct libusb_device_descriptor device_descriptor; + usbi_atomic_t attached; + + char * device_strings_utf8[LIBUSB_DEVICE_STRING_COUNT]; +}; + +struct libusb_device_handle { + /* lock protects claimed_interfaces */ + usbi_mutex_t lock; + unsigned long claimed_interfaces; + + struct list_head list; + struct libusb_device *dev; + int auto_detach_kernel_driver; +}; + +/* Function called by backend during device initialization to convert + * multi-byte fields in the device descriptor to host-endian format. + */ +static inline void usbi_localize_device_descriptor(struct libusb_device_descriptor *desc) +{ + desc->bcdUSB = libusb_le16_to_cpu(desc->bcdUSB); + desc->idVendor = libusb_le16_to_cpu(desc->idVendor); + desc->idProduct = libusb_le16_to_cpu(desc->idProduct); + desc->bcdDevice = libusb_le16_to_cpu(desc->bcdDevice); +} + +#if defined(HAVE_CLOCK_GETTIME) && !defined(__APPLE__) +static inline void usbi_get_monotonic_time(struct timespec *tp) +{ + ASSERT_EQ(clock_gettime(CLOCK_MONOTONIC, tp), 0); +} +static inline void usbi_get_real_time(struct timespec *tp) +{ + ASSERT_EQ(clock_gettime(CLOCK_REALTIME, tp), 0); +} +#else +/* If the platform doesn't provide the clock_gettime() function, the backend + * must provide its own clock implementations. Two clock functions are + * required: + * + * usbi_get_monotonic_time(): returns the time since an unspecified starting + * point (usually boot) that is monotonically + * increasing. + * usbi_get_real_time(): returns the time since system epoch. + */ +void usbi_get_monotonic_time(struct timespec *tp); +void usbi_get_real_time(struct timespec *tp); +#endif + +/* in-memory transfer layout: + * + * 1. os private data + * 2. struct usbi_transfer + * 3. struct libusb_transfer (which includes iso packets) [variable size] + * + * You can convert between them with the macros: + * TRANSFER_PRIV_TO_USBI_TRANSFER + * USBI_TRANSFER_TO_TRANSFER_PRIV + * USBI_TRANSFER_TO_LIBUSB_TRANSFER + * LIBUSB_TRANSFER_TO_USBI_TRANSFER + */ + +struct usbi_transfer { + /* this lock is held during libusb_submit_transfer() and + * libusb_cancel_transfer() (allowing the OS backend to prevent duplicate + * cancellation, submission-during-cancellation, etc). the OS backend + * should also take this lock in the handle_events path, to prevent the user + * cancelling the transfer from another thread while you are processing + * its completion (presumably there would be races within your OS backend + * if this were possible). + * Note paths taking both this and the flying_transfers_lock must + * always take the flying_transfers_lock first */ + usbi_mutex_t lock; + + struct list_head list; + struct list_head completed_list; + struct timespec timeout; + int transferred; + uint32_t stream_id; + uint32_t state_flags; /* Protected by usbi_transfer->lock */ + uint32_t timeout_flags; /* Protected by the flying_transfers_lock */ + + /* The device reference is held until destruction for logging + * even after dev_handle is set to NULL. */ + struct libusb_device *dev; + + void *priv; +}; + +enum usbi_transfer_state_flags { + /* Transfer successfully submitted by backend */ + USBI_TRANSFER_IN_FLIGHT = 1U << 0, + + /* Cancellation was requested via libusb_cancel_transfer() */ + USBI_TRANSFER_CANCELLING = 1U << 1, + + /* Operation on the transfer failed because the device disappeared */ + USBI_TRANSFER_DEVICE_DISAPPEARED = 1U << 2, +}; + +enum usbi_transfer_timeout_flags { + /* Set by backend submit_transfer() if the OS handles timeout */ + USBI_TRANSFER_OS_HANDLES_TIMEOUT = 1U << 0, + + /* The transfer timeout has been handled */ + USBI_TRANSFER_TIMEOUT_HANDLED = 1U << 1, + + /* The transfer timeout was successfully processed */ + USBI_TRANSFER_TIMED_OUT = 1U << 2, +}; + +#define TRANSFER_PRIV_TO_USBI_TRANSFER(transfer_priv) \ + ((struct usbi_transfer *) \ + ((unsigned char *)(transfer_priv) \ + + PTR_ALIGN(sizeof(*transfer_priv)))) + +#define USBI_TRANSFER_TO_TRANSFER_PRIV(itransfer) \ + ((unsigned char *) \ + ((unsigned char *)(itransfer) \ + - PTR_ALIGN(usbi_backend.transfer_priv_size))) + +#define USBI_TRANSFER_TO_LIBUSB_TRANSFER(itransfer) \ + ((struct libusb_transfer *) \ + ((unsigned char *)(itransfer) \ + + PTR_ALIGN(sizeof(struct usbi_transfer)))) + +#define LIBUSB_TRANSFER_TO_USBI_TRANSFER(transfer) \ + ((struct usbi_transfer *) \ + ((unsigned char *)(transfer) \ + - PTR_ALIGN(sizeof(struct usbi_transfer)))) + +#ifdef _MSC_VER +#pragma pack(push, 1) +#endif + +/* All standard descriptors have these 2 fields in common */ +struct usbi_descriptor_header { + uint8_t bLength; + uint8_t bDescriptorType; +} LIBUSB_PACKED; + +struct usbi_device_descriptor { + uint8_t bLength; + uint8_t bDescriptorType; + uint16_t bcdUSB; + uint8_t bDeviceClass; + uint8_t bDeviceSubClass; + uint8_t bDeviceProtocol; + uint8_t bMaxPacketSize0; + uint16_t idVendor; + uint16_t idProduct; + uint16_t bcdDevice; + uint8_t iManufacturer; + uint8_t iProduct; + uint8_t iSerialNumber; + uint8_t bNumConfigurations; +} LIBUSB_PACKED; + +struct usbi_configuration_descriptor { + uint8_t bLength; + uint8_t bDescriptorType; + uint16_t wTotalLength; + uint8_t bNumInterfaces; + uint8_t bConfigurationValue; + uint8_t iConfiguration; + uint8_t bmAttributes; + uint8_t bMaxPower; +} LIBUSB_PACKED; + +struct usbi_interface_descriptor { + uint8_t bLength; + uint8_t bDescriptorType; + uint8_t bInterfaceNumber; + uint8_t bAlternateSetting; + uint8_t bNumEndpoints; + uint8_t bInterfaceClass; + uint8_t bInterfaceSubClass; + uint8_t bInterfaceProtocol; + uint8_t iInterface; +} LIBUSB_PACKED; + +struct usbi_string_descriptor { + uint8_t bLength; + uint8_t bDescriptorType; + uint16_t wData[LIBUSB_FLEXIBLE_ARRAY]; +} LIBUSB_PACKED; + +struct usbi_bos_descriptor { + uint8_t bLength; + uint8_t bDescriptorType; + uint16_t wTotalLength; + uint8_t bNumDeviceCaps; +} LIBUSB_PACKED; + +#ifdef _MSC_VER +#pragma pack(pop) +#endif + +union usbi_config_desc_buf { + struct usbi_configuration_descriptor desc; + uint8_t buf[LIBUSB_DT_CONFIG_SIZE]; + uint16_t align; /* Force 2-byte alignment */ +}; + +union usbi_string_desc_buf { + struct usbi_string_descriptor desc; + uint8_t buf[255]; /* Some devices choke on size > 255 */ + uint16_t align; /* Force 2-byte alignment */ +}; + +union usbi_bos_desc_buf { + struct usbi_bos_descriptor desc; + uint8_t buf[LIBUSB_DT_BOS_SIZE]; + uint16_t align; /* Force 2-byte alignment */ +}; + +enum usbi_hotplug_flags { + /* This callback is interested in device arrivals */ + USBI_HOTPLUG_DEVICE_ARRIVED = LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED, + + /* This callback is interested in device removals */ + USBI_HOTPLUG_DEVICE_LEFT = LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT, + + /* IMPORTANT: The values for the below entries must start *after* + * the highest value of the above entries!!! + */ + + /* The vendor_id field is valid for matching */ + USBI_HOTPLUG_VENDOR_ID_VALID = (1U << 3), + + /* The product_id field is valid for matching */ + USBI_HOTPLUG_PRODUCT_ID_VALID = (1U << 4), + + /* The dev_class field is valid for matching */ + USBI_HOTPLUG_DEV_CLASS_VALID = (1U << 5), + + /* This callback has been unregistered and needs to be freed */ + USBI_HOTPLUG_NEEDS_FREE = (1U << 6), +}; + +struct usbi_hotplug_callback { + /* Flags that control how this callback behaves */ + uint8_t flags; + + /* Vendor ID to match (if flags says this is valid) */ + uint16_t vendor_id; + + /* Product ID to match (if flags says this is valid) */ + uint16_t product_id; + + /* Device class to match (if flags says this is valid) */ + uint8_t dev_class; + + /* Callback function to invoke for matching event/device */ + libusb_hotplug_callback_fn cb; + + /* Handle for this callback (used to match on deregister) */ + libusb_hotplug_callback_handle handle; + + /* User data that will be passed to the callback function */ + void *user_data; + + /* List this callback is registered in (ctx->hotplug_cbs) */ + struct list_head list; +}; + +struct usbi_hotplug_message { + /* The hotplug event that occurred */ + libusb_hotplug_event event; + + /* The device for which this hotplug event occurred */ + struct libusb_device *device; + + /* List this message is contained in (ctx->hotplug_msgs) */ + struct list_head list; +}; + +/* shared data and functions */ + +void usbi_hotplug_init(struct libusb_context *ctx); +void usbi_hotplug_exit(struct libusb_context *ctx); +void usbi_hotplug_notification(struct libusb_context *ctx, struct libusb_device *dev, + libusb_hotplug_event event); +void usbi_hotplug_process(struct libusb_context *ctx, struct list_head *hotplug_msgs); + +int usbi_io_init(struct libusb_context *ctx); +void usbi_io_exit(struct libusb_context *ctx); + +struct libusb_device *usbi_alloc_device(struct libusb_context *ctx, + unsigned long session_id); +struct libusb_device *usbi_get_device_by_session_id(struct libusb_context *ctx, + unsigned long session_id); +int usbi_sanitize_device(struct libusb_device *dev); +void usbi_handle_disconnect(struct libusb_context *ctx, struct libusb_device_handle *dev_handle); + +int usbi_handle_transfer_completion(struct usbi_transfer *itransfer, + enum libusb_transfer_status status); +int usbi_handle_transfer_cancellation(struct usbi_transfer *itransfer); +void usbi_signal_transfer_completion(struct usbi_transfer *itransfer); + +void usbi_attach_device(struct libusb_device *dev); +void usbi_detach_device(struct libusb_device *dev); + +void usbi_connect_device(struct libusb_device *dev); +void usbi_disconnect_device(struct libusb_device *dev); + +struct usbi_event_source_data { + usbi_os_handle_t os_handle; + short poll_events; +}; + +struct usbi_event_source { + struct usbi_event_source_data data; + struct list_head list; +}; + +int usbi_add_event_source(struct libusb_context *ctx, usbi_os_handle_t os_handle, + short poll_events); +void usbi_remove_event_source(struct libusb_context *ctx, usbi_os_handle_t os_handle); + +struct usbi_option { + int is_set; + union { + int ival; + libusb_log_cb log_cbval; + } arg; +}; + +/* OS event abstraction */ + +int usbi_create_event(usbi_event_t *event); +void usbi_destroy_event(usbi_event_t *event); +void usbi_signal_event(usbi_event_t *event); +void usbi_clear_event(usbi_event_t *event); + +#ifdef HAVE_OS_TIMER +int usbi_create_timer(usbi_timer_t *timer); +void usbi_destroy_timer(usbi_timer_t *timer); +int usbi_arm_timer(usbi_timer_t *timer, const struct timespec *timeout); +int usbi_disarm_timer(usbi_timer_t *timer); +#endif + +static inline int usbi_using_timer(struct libusb_context *ctx) +{ +#ifdef HAVE_OS_TIMER + return usbi_timer_valid(&ctx->timer); +#else + UNUSED(ctx); + return 0; +#endif +} + +struct usbi_reported_events { + union { + struct { + unsigned int event_triggered:1; +#ifdef HAVE_OS_TIMER + unsigned int timer_triggered:1; +#endif + }; + unsigned int event_bits; + }; + void *event_data; + unsigned int event_data_count; + unsigned int num_ready; +}; + +int usbi_alloc_event_data(struct libusb_context *ctx); +int usbi_wait_for_events(struct libusb_context *ctx, + struct usbi_reported_events *reported_events, int timeout_ms); + +/* accessor functions for structure private data */ + +static inline void *usbi_get_context_priv(struct libusb_context *ctx) +{ + return (unsigned char *)ctx + PTR_ALIGN(sizeof(*ctx)); +} + +static inline void *usbi_get_device_priv(struct libusb_device *dev) +{ + return (unsigned char *)dev + PTR_ALIGN(sizeof(*dev)); +} + +static inline void *usbi_get_device_handle_priv(struct libusb_device_handle *dev_handle) +{ + return (unsigned char *)dev_handle + PTR_ALIGN(sizeof(*dev_handle)); +} + +static inline void *usbi_get_transfer_priv(struct usbi_transfer *itransfer) +{ + return itransfer->priv; +} + +/* device discovery */ + +/* we traverse usbfs without knowing how many devices we are going to find. + * so we create this discovered_devs model which is similar to a linked-list + * which grows when required. it can be freed once discovery has completed, + * eliminating the need for a list node in the libusb_device structure + * itself. */ +struct discovered_devs { + size_t len; + size_t capacity; + struct libusb_device *devices[LIBUSB_FLEXIBLE_ARRAY]; +}; + +struct discovered_devs *discovered_devs_append( + struct discovered_devs *discdevs, struct libusb_device *dev); + +/* OS abstraction */ + +/* This is the interface that OS backends need to implement. + * All fields are mandatory, except ones explicitly noted as optional. */ +struct usbi_os_backend { + /* A human-readable name for your backend, e.g. "Linux usbfs" */ + const char *name; + + /* Binary mask for backend specific capabilities */ + uint32_t caps; + + /* Perform initialization of your backend. You might use this function + * to determine specific capabilities of the system, allocate required + * data structures for later, etc. + * + * This function is called when a libusb user initializes the library + * prior to use. Mutual exclusion with other init and exit calls is + * guaranteed when this function is called. + * + * Return 0 on success, or a LIBUSB_ERROR code on failure. + */ + int (*init)(struct libusb_context *ctx); + + /* Deinitialization. Optional. This function should destroy anything + * that was set up by init. + * + * This function is called when the user deinitializes the library. + * Mutual exclusion with other init and exit calls is guaranteed when + * this function is called. + */ + void (*exit)(struct libusb_context *ctx); + + /* Set a backend-specific option. Optional. + * + * This function is called when the user calls libusb_set_option() and + * the option is not handled by the core library. + * + * Return 0 on success, or a LIBUSB_ERROR code on failure. + */ + int (*set_option)(struct libusb_context *ctx, enum libusb_option option, + va_list args); + + /* Enumerate all the USB devices on the system, returning them in a list + * of discovered devices. + * + * Your implementation should enumerate all devices on the system, + * regardless of whether they have been seen before or not. + * + * When you have found a device, compute a session ID for it. The session + * ID should uniquely represent that particular device for that particular + * connection session since boot (i.e. if you disconnect and reconnect a + * device immediately after, it should be assigned a different session ID). + * If your OS cannot provide a unique session ID as described above, + * presenting a session ID of (bus_number << 8 | device_address) should + * be sufficient. Bus numbers and device addresses wrap and get reused, + * but that is an unlikely case. + * + * After computing a session ID for a device, call + * usbi_get_device_by_session_id(). This function checks if libusb already + * knows about the device, and if so, it provides you with a reference + * to a libusb_device structure for it. + * + * If usbi_get_device_by_session_id() returns NULL, it is time to allocate + * a new device structure for the device. Call usbi_alloc_device() to + * obtain a new libusb_device structure with reference count 1. Populate + * the bus_number and device_address attributes of the new device, and + * perform any other internal backend initialization you need to do. At + * this point, you should be ready to provide device descriptors and so + * on through the get_*_descriptor functions. Finally, call + * usbi_sanitize_device() to perform some final sanity checks on the + * device. Assuming all of the above succeeded, call + * usbi_connect_device() to add the device to the context's device list + * and make it discoverable by usbi_get_device_by_session_id(). + * If any of the above failed, remember to unreference the device that + * was returned by usbi_alloc_device(). + * + * At this stage we have a populated libusb_device structure (either one + * that was found earlier, or one that we have just allocated and + * populated). This can now be added to the discovered devices list + * using discovered_devs_append(). Note that discovered_devs_append() + * may reallocate the list, returning a new location for it, and also + * note that reallocation can fail. Your backend should handle these + * error conditions appropriately. + * + * This function should not generate any bus I/O and should not block. + * If I/O is required (e.g. reading the active configuration value), it is + * OK to ignore these suggestions :) + * + * This function is executed when the user wishes to retrieve a list + * of USB devices connected to the system. + * + * If the backend has hotplug support, this function is not used! + * + * Return 0 on success, or a LIBUSB_ERROR code on failure. + */ + int (*get_device_list)(struct libusb_context *ctx, + struct discovered_devs **discdevs); + + /* Retrieve a device string without needing to open the device. + * + * The string should be retrieved without opening the device + * and ideally without performing USB transactions to the device. + * Most operating systems read and cache the common string + * descriptors. Use the OS-specific calls to retrieve these strings. + * + * Since the USB string descriptor could be processed by the OS, + * this function returns a UTF-8 encoded string. + * + * The string will be returned untranslated or in the default OS language + * when supported by the OS and USB device. + * + * This function must not write more than length bytes into data, + * including the null terminator. + * + * Return: + * - The actual length in bytes including the null termintor on success. + * - LIBUSB_ERROR_NO_DEVICE if device not found. + * - LIBUSB_ERROR_INVALID_PARAM if any parameter is invalid. + * - another LIBUSB_ERROR code on other failure + */ + int (*get_device_string)(libusb_device *dev, + enum libusb_device_string_type string_type, char *data, int length); + + /* Apps which were written before hotplug support, may listen for + * hotplug events on their own and call libusb_get_device_list on + * device addition. In this case libusb_get_device_list will likely + * return a list without the new device in there, as the hotplug + * event thread will still be busy enumerating the device, which may + * take a while, or may not even have seen the event yet. + * + * To avoid this libusb_get_device_list will call this optional + * function for backends with hotplug support before copying + * ctx->usb_devs to the user. In this function the backend should + * ensure any pending hotplug events are fully processed before + * returning. + * + * Optional, should be implemented by backends with hotplug support. + */ + void (*hotplug_poll)(void); + + /* Wrap a platform-specific device handle for I/O and other USB + * operations. The device handle is preallocated for you. + * + * Your backend should allocate any internal resources required for I/O + * and other operations so that those operations can happen (hopefully) + * without hiccup. This is also a good place to inform libusb that it + * should monitor certain file descriptors related to this device - + * see the usbi_add_event_source() function. + * + * Your backend should also initialize the device structure + * (dev_handle->dev), which is NULL at the beginning of the call. + * + * This function should not generate any bus I/O and should not block. + * + * This function is called when the user attempts to wrap an existing + * platform-specific device handle for a device. + * + * Return: + * - 0 on success + * - LIBUSB_ERROR_ACCESS if the user has insufficient permissions + * - another LIBUSB_ERROR code on other failure + * + * Do not worry about freeing the handle on failed open, the upper layers + * do this for you. + */ + int (*wrap_sys_device)(struct libusb_context *ctx, + struct libusb_device_handle *dev_handle, intptr_t sys_dev); + + /* Open a device for I/O and other USB operations. The device handle + * is preallocated for you, you can retrieve the device in question + * through handle->dev. + * + * Your backend should allocate any internal resources required for I/O + * and other operations so that those operations can happen (hopefully) + * without hiccup. This is also a good place to inform libusb that it + * should monitor certain file descriptors related to this device - + * see the usbi_add_event_source() function. + * + * This function should not generate any bus I/O and should not block. + * + * This function is called when the user attempts to obtain a device + * handle for a device. + * + * Return: + * - 0 on success + * - LIBUSB_ERROR_ACCESS if the user has insufficient permissions + * - LIBUSB_ERROR_NO_DEVICE if the device has been disconnected since + * discovery + * - another LIBUSB_ERROR code on other failure + * + * Do not worry about freeing the handle on failed open, the upper layers + * do this for you. + */ + int (*open)(struct libusb_device_handle *dev_handle); + + /* Close a device such that the handle cannot be used again. Your backend + * should destroy any resources that were allocated in the open path. + * This may also be a good place to call usbi_remove_event_source() to + * inform libusb of any event sources associated with this device that + * should no longer be monitored. + * + * This function is called when the user closes a device handle. + */ + void (*close)(struct libusb_device_handle *dev_handle); + + /* Get the ACTIVE configuration descriptor for a device. + * + * The descriptor should be retrieved from memory, NOT via bus I/O to the + * device. This means that you may have to cache it in a private structure + * during get_device_list enumeration. You may also have to keep track + * of which configuration is active when the user changes it. + * + * This function is expected to write len bytes of data into buffer, which + * is guaranteed to be big enough. If you can only do a partial write, + * return an error code. + * + * This function is expected to return the descriptor in bus-endian format + * (LE). + * + * Return: + * - 0 on success + * - LIBUSB_ERROR_NOT_FOUND if the device is in unconfigured state + * - another LIBUSB_ERROR code on other failure + */ + int (*get_active_config_descriptor)(struct libusb_device *device, + void *buffer, size_t len); + + /* Get a specific configuration descriptor for a device. + * + * The descriptor should be retrieved from memory, NOT via bus I/O to the + * device. This means that you may have to cache it in a private structure + * during get_device_list enumeration. + * + * The requested descriptor is expressed as a zero-based index (i.e. 0 + * indicates that we are requesting the first descriptor). The index does + * not (necessarily) equal the bConfigurationValue of the configuration + * being requested. + * + * This function is expected to write len bytes of data into buffer, which + * is guaranteed to be big enough. If you can only do a partial write, + * return an error code. + * + * This function is expected to return the descriptor in bus-endian format + * (LE). + * + * Return the length read on success or a LIBUSB_ERROR code on failure. + */ + int (*get_config_descriptor)(struct libusb_device *device, + uint8_t config_index, void *buffer, size_t len); + + /* Like get_config_descriptor but then by bConfigurationValue instead + * of by index. + * + * Optional, if not present the core will call get_config_descriptor + * for all configs until it finds the desired bConfigurationValue. + * + * Returns a pointer to the raw-descriptor in *buffer, this memory + * is valid as long as device is valid. + * + * Returns the length of the returned raw-descriptor on success, + * or a LIBUSB_ERROR code on failure. + */ + int (*get_config_descriptor_by_value)(struct libusb_device *device, + uint8_t bConfigurationValue, void **buffer); + + /* Get the bConfigurationValue for the active configuration for a device. + * Optional. This should only be implemented if you can retrieve it from + * cache (don't generate I/O). + * + * If you cannot retrieve this from cache, either do not implement this + * function, or return LIBUSB_ERROR_NOT_SUPPORTED. This will cause + * libusb to retrieve the information through a standard control transfer. + * + * This function must be non-blocking. + * Return: + * - 0 on success + * - LIBUSB_ERROR_NO_DEVICE if the device has been disconnected since it + * was opened + * - LIBUSB_ERROR_NOT_SUPPORTED if the value cannot be retrieved without + * blocking + * - another LIBUSB_ERROR code on other failure. + */ + int (*get_configuration)(struct libusb_device_handle *dev_handle, uint8_t *config); + + /* Set the active configuration for a device. + * + * A configuration value of -1 should put the device in unconfigured state. + * + * This function can block. + * + * Return: + * - 0 on success + * - LIBUSB_ERROR_NOT_FOUND if the configuration does not exist + * - LIBUSB_ERROR_BUSY if interfaces are currently claimed (and hence + * configuration cannot be changed) + * - LIBUSB_ERROR_NO_DEVICE if the device has been disconnected since it + * was opened + * - another LIBUSB_ERROR code on other failure. + */ + int (*set_configuration)(struct libusb_device_handle *dev_handle, int config); + + /* Claim an interface. When claimed, the application can then perform + * I/O to an interface's endpoints. + * + * This function should not generate any bus I/O and should not block. + * Interface claiming is a logical operation that simply ensures that + * no other drivers/applications are using the interface, and after + * claiming, no other drivers/applications can use the interface because + * we now "own" it. + * + * This function gets called with dev_handle->lock locked! + * + * Return: + * - 0 on success + * - LIBUSB_ERROR_NOT_FOUND if the interface does not exist + * - LIBUSB_ERROR_BUSY if the interface is in use by another driver/app + * - LIBUSB_ERROR_NO_DEVICE if the device has been disconnected since it + * was opened + * - another LIBUSB_ERROR code on other failure + */ + int (*claim_interface)(struct libusb_device_handle *dev_handle, uint8_t interface_number); + + /* Release a previously claimed interface. + * + * This function should also generate a SET_INTERFACE control request, + * resetting the alternate setting of that interface to 0. It's OK for + * this function to block as a result. + * + * You will only ever be asked to release an interface which was + * successfully claimed earlier. + * + * This function gets called with dev_handle->lock locked! + * + * Return: + * - 0 on success + * - LIBUSB_ERROR_NO_DEVICE if the device has been disconnected since it + * was opened + * - another LIBUSB_ERROR code on other failure + */ + int (*release_interface)(struct libusb_device_handle *dev_handle, uint8_t interface_number); + + /* Set the alternate setting for an interface. + * + * You will only ever be asked to set the alternate setting for an + * interface which was successfully claimed earlier. + * + * It's OK for this function to block. + * + * Return: + * - 0 on success + * - LIBUSB_ERROR_NOT_FOUND if the alternate setting does not exist + * - LIBUSB_ERROR_NO_DEVICE if the device has been disconnected since it + * was opened + * - another LIBUSB_ERROR code on other failure + */ + int (*set_interface_altsetting)(struct libusb_device_handle *dev_handle, + uint8_t interface_number, uint8_t altsetting); + + /* Clear a halt/stall condition on an endpoint. + * + * It's OK for this function to block. + * + * Return: + * - 0 on success + * - LIBUSB_ERROR_NOT_FOUND if the endpoint does not exist + * - LIBUSB_ERROR_NO_DEVICE if the device has been disconnected since it + * was opened + * - another LIBUSB_ERROR code on other failure + */ + int (*clear_halt)(struct libusb_device_handle *dev_handle, + unsigned char endpoint); + + /* Perform a USB port reset to reinitialize a device. Optional. + * + * If possible, the device handle should still be usable after the reset + * completes, assuming that the device descriptors did not change during + * reset and all previous interface state can be restored. + * + * If something changes, or you cannot easily locate/verify the reset + * device, return LIBUSB_ERROR_NOT_FOUND. This prompts the application + * to close the old handle and re-enumerate the device. + * + * Return: + * - 0 on success + * - LIBUSB_ERROR_NOT_FOUND if re-enumeration is required, or if the device + * has been disconnected since it was opened + * - another LIBUSB_ERROR code on other failure + */ + int (*reset_device)(struct libusb_device_handle *dev_handle); + + /* Alloc num_streams usb3 bulk streams on the passed in endpoints */ + int (*alloc_streams)(struct libusb_device_handle *dev_handle, + uint32_t num_streams, unsigned char *endpoints, int num_endpoints); + + /* Free usb3 bulk streams allocated with alloc_streams */ + int (*free_streams)(struct libusb_device_handle *dev_handle, + unsigned char *endpoints, int num_endpoints); + + /* Allocate persistent DMA memory for the given device, suitable for + * zerocopy. May return NULL on failure. Optional to implement. + */ + void *(*dev_mem_alloc)(struct libusb_device_handle *handle, size_t len); + + /* Free memory allocated by dev_mem_alloc. */ + int (*dev_mem_free)(struct libusb_device_handle *handle, void *buffer, + size_t len); + + /* Determine if a kernel driver is active on an interface. Optional. + * + * The presence of a kernel driver on an interface indicates that any + * calls to claim_interface would fail with the LIBUSB_ERROR_BUSY code. + * + * Return: + * - 0 if no driver is active + * - 1 if a driver is active + * - LIBUSB_ERROR_NO_DEVICE if the device has been disconnected since it + * was opened + * - another LIBUSB_ERROR code on other failure + */ + int (*kernel_driver_active)(struct libusb_device_handle *dev_handle, + uint8_t interface_number); + + /* Detach a kernel driver from an interface. Optional. + * + * After detaching a kernel driver, the interface should be available + * for claim. + * + * Return: + * - 0 on success + * - LIBUSB_ERROR_NOT_FOUND if no kernel driver was active + * - LIBUSB_ERROR_INVALID_PARAM if the interface does not exist + * - LIBUSB_ERROR_NO_DEVICE if the device has been disconnected since it + * was opened + * - another LIBUSB_ERROR code on other failure + */ + int (*detach_kernel_driver)(struct libusb_device_handle *dev_handle, + uint8_t interface_number); + + /* Attach a kernel driver to an interface. Optional. + * + * Reattach a kernel driver to the device. + * + * Return: + * - 0 on success + * - LIBUSB_ERROR_NOT_FOUND if no kernel driver was active + * - LIBUSB_ERROR_INVALID_PARAM if the interface does not exist + * - LIBUSB_ERROR_NO_DEVICE if the device has been disconnected since it + * was opened + * - LIBUSB_ERROR_BUSY if a program or driver has claimed the interface, + * preventing reattachment + * - another LIBUSB_ERROR code on other failure + */ + int (*attach_kernel_driver)(struct libusb_device_handle *dev_handle, + uint8_t interface_number); + + /** Check if RAW_IO is supported by an endpoint. + * + * Return: + * - 1 if yes + * - 0 if no + * - a LIBUSB_ERROR code on failure + */ + int (*endpoint_supports_raw_io)(struct libusb_device_handle* dev_handle, + uint8_t endpoint); + + /** Enable/disable RAW_IO for an endpoint. + * + * Return: + * - 0 on success + * - LIBUSB_ERROR_NOT_SUPPORTED if RAW_IO is not supported by the endpoint + * - another LIBUSB_ERROR code on other failure + */ + int (*endpoint_set_raw_io)(struct libusb_device_handle* dev_handle, + uint8_t endpoint, int enable); + + /* Retrieve the maximum transfer size in bytes supported for WinUSB RAW_IO + * for an inbound bulk or interrupt endpoint on an open device. Optional. + * + * Return: + * - a positive maximum transfer size on success + * - a LIBUSB_ERROR code on failure + */ + int (*get_max_raw_io_transfer_size)( + struct libusb_device_handle *dev_handle, + uint8_t endpoint); + + /* Destroy a device. Optional. + * + * This function is called when the last reference to a device is + * destroyed. It should free any resources allocated in the get_device_list + * path. + */ + void (*destroy_device)(struct libusb_device *dev); + + /* Submit a transfer. Your implementation should take the transfer, + * morph it into whatever form your platform requires, and submit it + * asynchronously. + * + * This function must not block. + * + * This function gets called with itransfer->lock locked! + * + * Return: + * - 0 on success + * - LIBUSB_ERROR_NO_DEVICE if the device has been disconnected + * - another LIBUSB_ERROR code on other failure + */ + int (*submit_transfer)(struct usbi_transfer *itransfer); + + /* Cancel a previously submitted transfer. + * + * This function must not block. The transfer cancellation must complete + * later, resulting in a call to usbi_handle_transfer_cancellation() + * from the context of handle_events. + * + * This function gets called with itransfer->lock locked! + */ + int (*cancel_transfer)(struct usbi_transfer *itransfer); + + /* Clear a transfer as if it has completed or cancelled, but do not + * report any completion/cancellation to the library. You should free + * all private data from the transfer as if you were just about to report + * completion or cancellation. + * + * This function might seem a bit out of place. It is used when libusb + * detects a disconnected device - it calls this function for all pending + * transfers before reporting completion (with the disconnect code) to + * the user. Maybe we can improve upon this internal interface in future. + */ + void (*clear_transfer_priv)(struct usbi_transfer *itransfer); + + /* Handle any pending events on event sources. Optional. + * + * Provide this function when event sources directly indicate device + * or transfer activity. If your backend does not have such event sources, + * implement the handle_transfer_completion function below. + * + * This involves monitoring any active transfers and processing their + * completion or cancellation. + * + * The function is passed a pointer that represents platform-specific + * data for monitoring event sources (size count). This data is to be + * (re)allocated as necessary when event sources are modified. + * The num_ready parameter indicates the number of event sources that + * have reported events. This should be enough information for you to + * determine which actions need to be taken on the currently active + * transfers. + * + * For any cancelled transfers, call usbi_handle_transfer_cancellation(). + * For completed transfers, call usbi_handle_transfer_completion(). + * For control/bulk/interrupt transfers, populate the "transferred" + * element of the appropriate usbi_transfer structure before calling the + * above functions. For isochronous transfers, populate the status and + * transferred fields of the iso packet descriptors of the transfer. + * + * This function should also be able to detect disconnection of the + * device, reporting that situation with usbi_handle_disconnect(). + * + * When processing an event related to a transfer, you probably want to + * take usbi_transfer.lock to prevent races. See the documentation for + * the usbi_transfer structure. + * + * Return 0 on success, or a LIBUSB_ERROR code on failure. + */ + int (*handle_events)(struct libusb_context *ctx, + void *event_data, unsigned int count, unsigned int num_ready); + + /* Handle transfer completion. Optional. + * + * Provide this function when there are no event sources available that + * directly indicate device or transfer activity. If your backend does + * have such event sources, implement the handle_events function above. + * + * Your backend must tell the library when a transfer has completed by + * calling usbi_signal_transfer_completion(). You should store any private + * information about the transfer and its completion status in the transfer's + * private backend data. + * + * During event handling, this function will be called on each transfer for + * which usbi_signal_transfer_completion() was called. + * + * For any cancelled transfers, call usbi_handle_transfer_cancellation(). + * For completed transfers, call usbi_handle_transfer_completion(). + * For control/bulk/interrupt transfers, populate the "transferred" + * element of the appropriate usbi_transfer structure before calling the + * above functions. For isochronous transfers, populate the status and + * transferred fields of the iso packet descriptors of the transfer. + * + * Return 0 on success, or a LIBUSB_ERROR code on failure. + */ + int (*handle_transfer_completion)(struct usbi_transfer *itransfer); + + /* Number of bytes to reserve for per-context private backend data. + * This private data area is accessible by calling + * usbi_get_context_priv() on the libusb_context instance. + */ + size_t context_priv_size; + + /* Number of bytes to reserve for per-device private backend data. + * This private data area is accessible by calling + * usbi_get_device_priv() on the libusb_device instance. + */ + size_t device_priv_size; + + /* Number of bytes to reserve for per-handle private backend data. + * This private data area is accessible by calling + * usbi_get_device_handle_priv() on the libusb_device_handle instance. + */ + size_t device_handle_priv_size; + + /* Number of bytes to reserve for per-transfer private backend data. + * This private data area is accessible by calling + * usbi_get_transfer_priv() on the usbi_transfer instance. + */ + size_t transfer_priv_size; +}; + +extern const struct usbi_os_backend usbi_backend; + +#define for_each_context(c) \ + for_each_helper(c, &active_contexts_list, struct libusb_context) + +#define for_each_device(ctx, d) \ + for_each_helper(d, &(ctx)->usb_devs, struct libusb_device) + +#define for_each_device_safe(ctx, d, n) \ + for_each_safe_helper(d, n, &(ctx)->usb_devs, struct libusb_device) + +#define for_each_open_device(ctx, h) \ + for_each_helper(h, &(ctx)->open_devs, struct libusb_device_handle) + +#define __for_each_transfer(list, t) \ + for_each_helper(t, (list), struct usbi_transfer) + +#define for_each_transfer(ctx, t) \ + __for_each_transfer(&(ctx)->flying_transfers, t) + +#define __for_each_transfer_safe(list, t, n) \ + for_each_safe_helper(t, n, (list), struct usbi_transfer) + +#define for_each_transfer_safe(ctx, t, n) \ + __for_each_transfer_safe(&(ctx)->flying_transfers, t, n) + +#define __for_each_completed_transfer_safe(list, t, n) \ + list_for_each_entry_safe(t, n, (list), completed_list, struct usbi_transfer) + +#define for_each_event_source(ctx, e) \ + for_each_helper(e, &(ctx)->event_sources, struct usbi_event_source) + +#define for_each_removed_event_source(ctx, e) \ + for_each_helper(e, &(ctx)->removed_event_sources, struct usbi_event_source) + +#define for_each_removed_event_source_safe(ctx, e, n) \ + for_each_safe_helper(e, n, &(ctx)->removed_event_sources, struct usbi_event_source) + +#define for_each_hotplug_cb(ctx, c) \ + for_each_helper(c, &(ctx)->hotplug_cbs, struct usbi_hotplug_callback) + +#define for_each_hotplug_cb_safe(ctx, c, n) \ + for_each_safe_helper(c, n, &(ctx)->hotplug_cbs, struct usbi_hotplug_callback) + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/usbaudio/src/main/cpp/external/libusb/libusb/os/events_posix.c b/usbaudio/src/main/cpp/external/libusb/libusb/os/events_posix.c new file mode 100644 index 000000000..bf984b91a --- /dev/null +++ b/usbaudio/src/main/cpp/external/libusb/libusb/os/events_posix.c @@ -0,0 +1,340 @@ +/* + * libusb event abstraction on POSIX platforms + * + * Copyright © 2020 Chris Dickens + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "libusbi.h" + +#include +#include +#ifdef HAVE_EVENTFD +#include +#endif +#ifdef HAVE_TIMERFD +#include +#endif + +#ifdef __EMSCRIPTEN__ +/* On Emscripten `pipe` does not conform to the spec and does not block + * until events are available, which makes it unusable for event system + * and often results in deadlocks when `pipe` is in a loop like it is + * in libusb. + * + * Therefore use a custom event system based on browser event emitters. */ +#include +#include +#include + +EM_ASYNC_JS(void, em_libusb_wait_async, (const _Atomic int* ptr, int expected_value, int timeout), { + await Atomics.waitAsync(HEAP32, ptr >> 2, expected_value, timeout).value; +}); + +static void em_libusb_wait(const _Atomic int *ptr, int expected_value, int timeout) +{ + if (emscripten_is_main_runtime_thread()) { + em_libusb_wait_async(ptr, expected_value, timeout); + } else { + emscripten_atomic_wait_u32((int*)ptr, expected_value, 1000000LL * timeout); + } +} +#endif +#include + +#ifdef HAVE_EVENTFD +#define EVENT_READ_FD(e) ((e)->eventfd) +#define EVENT_WRITE_FD(e) ((e)->eventfd) +#else +#define EVENT_READ_FD(e) ((e)->pipefd[0]) +#define EVENT_WRITE_FD(e) ((e)->pipefd[1]) +#endif + +#ifdef HAVE_NFDS_T +typedef nfds_t usbi_nfds_t; +#else +typedef unsigned int usbi_nfds_t; +#endif + +int usbi_create_event(usbi_event_t *event) +{ +#ifdef HAVE_EVENTFD + event->eventfd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC); + if (event->eventfd == -1) { + usbi_err(NULL, "failed to create eventfd, errno=%d", errno); + return LIBUSB_ERROR_OTHER; + } + + return 0; +#else +#if defined(HAVE_PIPE2) + int ret = pipe2(event->pipefd, O_CLOEXEC); +#else + int ret = pipe(event->pipefd); +#endif + + if (ret != 0) { + usbi_err(NULL, "failed to create pipe, errno=%d", errno); + return LIBUSB_ERROR_OTHER; + } + +#if !defined(HAVE_PIPE2) && defined(FD_CLOEXEC) + ret = fcntl(event->pipefd[0], F_GETFD); + if (ret == -1) { + usbi_err(NULL, "failed to get pipe fd flags, errno=%d", errno); + goto err_close_pipe; + } + ret = fcntl(event->pipefd[0], F_SETFD, ret | FD_CLOEXEC); + if (ret == -1) { + usbi_err(NULL, "failed to set pipe fd flags, errno=%d", errno); + goto err_close_pipe; + } + + ret = fcntl(event->pipefd[1], F_GETFD); + if (ret == -1) { + usbi_err(NULL, "failed to get pipe fd flags, errno=%d", errno); + goto err_close_pipe; + } + ret = fcntl(event->pipefd[1], F_SETFD, ret | FD_CLOEXEC); + if (ret == -1) { + usbi_err(NULL, "failed to set pipe fd flags, errno=%d", errno); + goto err_close_pipe; + } +#endif + + ret = fcntl(event->pipefd[1], F_GETFL); + if (ret == -1) { + usbi_err(NULL, "failed to get pipe fd status flags, errno=%d", errno); + goto err_close_pipe; + } + ret = fcntl(event->pipefd[1], F_SETFL, ret | O_NONBLOCK); + if (ret == -1) { + usbi_err(NULL, "failed to set pipe fd status flags, errno=%d", errno); + goto err_close_pipe; + } + + return 0; + +err_close_pipe: + close(event->pipefd[1]); + close(event->pipefd[0]); + return LIBUSB_ERROR_OTHER; +#endif +} + +void usbi_destroy_event(usbi_event_t *event) +{ +#ifdef HAVE_EVENTFD + if (close(event->eventfd) == -1) + usbi_warn(NULL, "failed to close eventfd, errno=%d", errno); +#else + if (close(event->pipefd[1]) == -1) + usbi_warn(NULL, "failed to close pipe write end, errno=%d", errno); + if (close(event->pipefd[0]) == -1) + usbi_warn(NULL, "failed to close pipe read end, errno=%d", errno); +#endif +} + +void usbi_signal_event(usbi_event_t *event) +{ + uint64_t dummy = 1; + ssize_t r; + + r = write(EVENT_WRITE_FD(event), &dummy, sizeof(dummy)); + if (r != sizeof(dummy)) + usbi_warn(NULL, "event write failed"); +#ifdef __EMSCRIPTEN__ + event->has_event = 1; + emscripten_atomic_notify(&event->has_event, EMSCRIPTEN_NOTIFY_ALL_WAITERS); +#endif +} + +void usbi_clear_event(usbi_event_t *event) +{ + uint64_t dummy; + ssize_t r; + + r = read(EVENT_READ_FD(event), &dummy, sizeof(dummy)); + if (r != sizeof(dummy)) + usbi_warn(NULL, "event read failed"); +#ifdef __EMSCRIPTEN__ + event->has_event = 0; +#endif +} + +#ifdef HAVE_TIMERFD +int usbi_create_timer(usbi_timer_t *timer) +{ + timer->timerfd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC); + if (timer->timerfd == -1) { + usbi_warn(NULL, "failed to create timerfd, errno=%d", errno); + return LIBUSB_ERROR_OTHER; + } + + return 0; +} + +void usbi_destroy_timer(usbi_timer_t *timer) +{ + if (close(timer->timerfd) == -1) + usbi_warn(NULL, "failed to close timerfd, errno=%d", errno); +} + +int usbi_arm_timer(usbi_timer_t *timer, const struct timespec *timeout) +{ + const struct itimerspec it = { { 0, 0 }, { timeout->tv_sec, timeout->tv_nsec } }; + + if (timerfd_settime(timer->timerfd, TFD_TIMER_ABSTIME, &it, NULL) == -1) { + usbi_warn(NULL, "failed to arm timerfd, errno=%d", errno); + return LIBUSB_ERROR_OTHER; + } + + return 0; +} + +int usbi_disarm_timer(usbi_timer_t *timer) +{ + const struct itimerspec it = { { 0, 0 }, { 0, 0 } }; + + if (timerfd_settime(timer->timerfd, 0, &it, NULL) == -1) { + usbi_warn(NULL, "failed to disarm timerfd, errno=%d", errno); + return LIBUSB_ERROR_OTHER; + } + + return 0; +} +#endif + +int usbi_alloc_event_data(struct libusb_context *ctx) +{ + struct usbi_event_source *ievent_source; + struct pollfd *fds; + size_t i = 0; + + if (ctx->event_data) { + free(ctx->event_data); + ctx->event_data = NULL; + } + + ctx->event_data_cnt = 0; + for_each_event_source(ctx, ievent_source) + ctx->event_data_cnt++; + + fds = calloc(ctx->event_data_cnt, sizeof(*fds)); + if (!fds) + return LIBUSB_ERROR_NO_MEM; + + for_each_event_source(ctx, ievent_source) { + fds[i].fd = ievent_source->data.os_handle; + fds[i].events = ievent_source->data.poll_events; + i++; + } + + ctx->event_data = fds; + return 0; +} + +int usbi_wait_for_events(struct libusb_context *ctx, + struct usbi_reported_events *reported_events, int timeout_ms) +{ + struct pollfd *fds = ctx->event_data; + usbi_nfds_t nfds = (usbi_nfds_t)ctx->event_data_cnt; + int internal_fds, num_ready; + + usbi_dbg(ctx, "poll() %u fds with timeout in %dms", (unsigned int)nfds, timeout_ms); +#ifdef __EMSCRIPTEN__ + /* Emscripten's poll doesn't actually block, so we need to use an + * out-of-band waiting signal. */ + em_libusb_wait(&ctx->event.has_event, 0, timeout_ms); + /* Emscripten ignores timeout_ms, but set it to 0 for future-proofing + * in case they ever implement real poll. */ + timeout_ms = 0; +#endif + num_ready = poll(fds, nfds, timeout_ms); + usbi_dbg(ctx, "poll() returned %d", num_ready); + if (num_ready == 0) { + if (usbi_using_timer(ctx)) + goto done; + return LIBUSB_ERROR_TIMEOUT; + } else if (num_ready == -1) { + if (errno == EINTR) + return LIBUSB_ERROR_INTERRUPTED; + usbi_err(ctx, "poll() failed, errno=%d", errno); + return LIBUSB_ERROR_IO; + } + + /* fds[0] is always the internal signalling event */ + if (fds[0].revents) { + reported_events->event_triggered = 1; + num_ready--; + } else { + reported_events->event_triggered = 0; + } + +#ifdef HAVE_OS_TIMER + /* on timer configurations, fds[1] is the timer */ + if (usbi_using_timer(ctx) && fds[1].revents) { + reported_events->timer_triggered = 1; + num_ready--; + } else { + reported_events->timer_triggered = 0; + } +#endif + + if (!num_ready) + goto done; + + /* the backend will never need to attempt to handle events on the + * library's internal file descriptors, so we determine how many are + * in use internally for this context and skip these when passing any + * remaining pollfds to the backend. */ + internal_fds = usbi_using_timer(ctx) ? 2 : 1; + fds += internal_fds; + nfds -= internal_fds; + + usbi_mutex_lock(&ctx->event_data_lock); + if (ctx->event_flags & USBI_EVENT_EVENT_SOURCES_MODIFIED) { + struct usbi_event_source *ievent_source; + + for_each_removed_event_source(ctx, ievent_source) { + usbi_nfds_t n; + + for (n = 0; n < nfds; n++) { + if (ievent_source->data.os_handle != fds[n].fd) + continue; + if (!fds[n].revents) + continue; + /* pollfd was removed between the creation of the fds array and + * here. remove triggered revent as it is no longer relevant. */ + usbi_dbg(ctx, "fd %d was removed, ignoring raised events", fds[n].fd); + fds[n].revents = 0; + num_ready--; + break; + } + } + } + usbi_mutex_unlock(&ctx->event_data_lock); + + if (num_ready) { + assert(num_ready > 0); + reported_events->event_data = fds; + reported_events->event_data_count = (unsigned int)nfds; + } + +done: + reported_events->num_ready = num_ready; + return LIBUSB_SUCCESS; +} diff --git a/usbaudio/src/main/cpp/external/libusb/libusb/os/events_posix.h b/usbaudio/src/main/cpp/external/libusb/libusb/os/events_posix.h new file mode 100644 index 000000000..4bd7f0fa2 --- /dev/null +++ b/usbaudio/src/main/cpp/external/libusb/libusb/os/events_posix.h @@ -0,0 +1,62 @@ +/* + * libusb event abstraction on POSIX platforms + * + * Copyright © 2020 Chris Dickens + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef LIBUSB_EVENTS_POSIX_H +#define LIBUSB_EVENTS_POSIX_H + +#include + +typedef int usbi_os_handle_t; +#define USBI_OS_HANDLE_FORMAT_STRING "fd %d" + +#ifdef HAVE_EVENTFD +typedef struct usbi_event { + int eventfd; +} usbi_event_t; +#define USBI_EVENT_OS_HANDLE(e) ((e)->eventfd) +#define USBI_EVENT_POLL_EVENTS POLLIN +#define USBI_INVALID_EVENT { -1 } +#else +typedef struct usbi_event { + int pipefd[2]; +#ifdef __EMSCRIPTEN__ + _Atomic int has_event; +#endif +} usbi_event_t; +#define USBI_EVENT_OS_HANDLE(e) ((e)->pipefd[0]) +#define USBI_EVENT_POLL_EVENTS POLLIN +#define USBI_INVALID_EVENT { { -1, -1 } } +#endif + +#ifdef HAVE_TIMERFD +#define HAVE_OS_TIMER 1 +typedef struct usbi_timer { + int timerfd; +} usbi_timer_t; +#define USBI_TIMER_OS_HANDLE(t) ((t)->timerfd) +#define USBI_TIMER_POLL_EVENTS POLLIN + +static inline int usbi_timer_valid(usbi_timer_t *timer) +{ + return timer->timerfd >= 0; +} +#endif + +#endif diff --git a/usbaudio/src/main/cpp/external/libusb/libusb/os/linux_netlink.c b/usbaudio/src/main/cpp/external/libusb/libusb/os/linux_netlink.c new file mode 100644 index 000000000..899084f22 --- /dev/null +++ b/usbaudio/src/main/cpp/external/libusb/libusb/os/linux_netlink.c @@ -0,0 +1,401 @@ +/* -*- Mode: C; c-basic-offset:8 ; indent-tabs-mode:t -*- */ +/* + * Linux usbfs backend for libusb + * Copyright (C) 2007-2009 Daniel Drake + * Copyright (c) 2001 Johannes Erdfelt + * Copyright (c) 2013 Nathan Hjelm + * Copyright (c) 2016 Chris Dickens + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "libusbi.h" +#include "linux_usbfs.h" + +#include +#include +#include +#include +#include +#include + +#ifdef HAVE_ASM_TYPES_H +#include +#endif +#include +#include + +#define NL_GROUP_KERNEL 1 + +#ifndef SOCK_CLOEXEC +#define SOCK_CLOEXEC 0 +#endif + +#ifndef SOCK_NONBLOCK +#define SOCK_NONBLOCK 0 +#endif + +static int linux_netlink_socket = -1; +static usbi_event_t netlink_control_event = USBI_INVALID_EVENT; +static pthread_t libusb_linux_event_thread; + +static void *linux_netlink_event_thread_main(void *arg); + +static int set_fd_cloexec_nb(int fd, int socktype) +{ + int flags; + +#if defined(FD_CLOEXEC) + /* Make sure the netlink socket file descriptor is marked as CLOEXEC */ + if (!(socktype & SOCK_CLOEXEC)) { + flags = fcntl(fd, F_GETFD); + if (flags == -1) { + usbi_err(NULL, "failed to get netlink fd flags, errno=%d", errno); + return -1; + } + + if (fcntl(fd, F_SETFD, flags | FD_CLOEXEC) == -1) { + usbi_err(NULL, "failed to set netlink fd flags, errno=%d", errno); + return -1; + } + } +#endif + + /* Make sure the netlink socket is non-blocking */ + if (!(socktype & SOCK_NONBLOCK)) { + flags = fcntl(fd, F_GETFL); + if (flags == -1) { + usbi_err(NULL, "failed to get netlink fd status flags, errno=%d", errno); + return -1; + } + + if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) { + usbi_err(NULL, "failed to set netlink fd status flags, errno=%d", errno); + return -1; + } + } + + return 0; +} + +int linux_netlink_start_event_monitor(void) +{ + struct sockaddr_nl sa_nl = { .nl_family = AF_NETLINK, .nl_groups = NL_GROUP_KERNEL }; + int socktype = SOCK_RAW | SOCK_NONBLOCK | SOCK_CLOEXEC; + int opt = 1; + int ret; + + linux_netlink_socket = socket(PF_NETLINK, socktype, NETLINK_KOBJECT_UEVENT); + if (linux_netlink_socket == -1 && errno == EINVAL) { + usbi_dbg(NULL, "failed to create netlink socket of type %d, attempting SOCK_RAW", socktype); + socktype = SOCK_RAW; + linux_netlink_socket = socket(PF_NETLINK, socktype, NETLINK_KOBJECT_UEVENT); + } + + if (linux_netlink_socket == -1) { + usbi_err(NULL, "failed to create netlink socket, errno=%d", errno); + goto err; + } + + ret = set_fd_cloexec_nb(linux_netlink_socket, socktype); + if (ret == -1) + goto err_close_socket; + + ret = bind(linux_netlink_socket, (struct sockaddr *)&sa_nl, sizeof(sa_nl)); + if (ret == -1) { + usbi_err(NULL, "failed to bind netlink socket, errno=%d", errno); + goto err_close_socket; + } + + ret = setsockopt(linux_netlink_socket, SOL_SOCKET, SO_PASSCRED, &opt, sizeof(opt)); + if (ret == -1) { + usbi_err(NULL, "failed to set netlink socket SO_PASSCRED option, errno=%d", errno); + goto err_close_socket; + } + + ret = usbi_create_event(&netlink_control_event); + if (ret) { + usbi_err(NULL, "failed to create netlink control event"); + goto err_close_socket; + } + + ret = pthread_create(&libusb_linux_event_thread, NULL, linux_netlink_event_thread_main, NULL); + if (ret != 0) { + usbi_err(NULL, "failed to create netlink event thread (%d)", ret); + goto err_destroy_event; + } + + return LIBUSB_SUCCESS; + +err_destroy_event: + usbi_destroy_event(&netlink_control_event); + netlink_control_event = (usbi_event_t)USBI_INVALID_EVENT; +err_close_socket: + close(linux_netlink_socket); + linux_netlink_socket = -1; +err: + return LIBUSB_ERROR_OTHER; +} + +int linux_netlink_stop_event_monitor(void) +{ + int ret; + + assert(linux_netlink_socket != -1); + + /* Signal the control event and wait for the thread to exit */ + usbi_signal_event(&netlink_control_event); + + ret = pthread_join(libusb_linux_event_thread, NULL); + if (ret) + usbi_warn(NULL, "failed to join netlink event thread (%d)", ret); + + usbi_destroy_event(&netlink_control_event); + netlink_control_event = (usbi_event_t)USBI_INVALID_EVENT; + + close(linux_netlink_socket); + linux_netlink_socket = -1; + + return LIBUSB_SUCCESS; +} + +static const char *netlink_message_parse(const char *buffer, size_t len, const char *key) +{ + const char *end = buffer + len; + size_t keylen = strlen(key); + + while (buffer < end && *buffer) { + if (strncmp(buffer, key, keylen) == 0 && buffer[keylen] == '=') + return buffer + keylen + 1; + buffer += strlen(buffer) + 1; + } + + return NULL; +} + +/* parse parts of netlink message common to both libudev and the kernel */ +static int linux_netlink_parse(const char *buffer, size_t len, int *detached, + const char **sys_name, uint8_t *busnum, uint8_t *devaddr) +{ + const char *tmp, *slash; + + errno = 0; + + *sys_name = NULL; + *detached = 0; + *busnum = 0; + *devaddr = 0; + + tmp = netlink_message_parse(buffer, len, "ACTION"); + if (!tmp) { + return -1; + } else if (strcmp(tmp, "remove") == 0) { + *detached = 1; + } else if (strcmp(tmp, "add") != 0) { + usbi_dbg(NULL, "unknown device action %s", tmp); + return -1; + } + + /* check that this is a usb message */ + tmp = netlink_message_parse(buffer, len, "SUBSYSTEM"); + if (!tmp || strcmp(tmp, "usb") != 0) { + /* not usb. ignore */ + return -1; + } + + /* check that this is an actual usb device */ + tmp = netlink_message_parse(buffer, len, "DEVTYPE"); + if (!tmp || strcmp(tmp, "usb_device") != 0) { + /* not usb. ignore */ + return -1; + } + + tmp = netlink_message_parse(buffer, len, "BUSNUM"); + if (tmp) { + *busnum = (uint8_t)(strtoul(tmp, NULL, 10) & 0xff); + if (errno) { + errno = 0; + return -1; + } + + tmp = netlink_message_parse(buffer, len, "DEVNUM"); + if (NULL == tmp) + return -1; + + *devaddr = (uint8_t)(strtoul(tmp, NULL, 10) & 0xff); + if (errno) { + errno = 0; + return -1; + } + } else { + /* no bus number. try "DEVICE" */ + tmp = netlink_message_parse(buffer, len, "DEVICE"); + if (!tmp) { + /* not usb. ignore */ + return -1; + } + + /* Parse a device path such as /dev/bus/usb/003/004 */ + slash = strrchr(tmp, '/'); + if (!slash) + return -1; + + *busnum = (uint8_t)(strtoul(slash - 3, NULL, 10) & 0xff); + if (errno) { + errno = 0; + return -1; + } + + *devaddr = (uint8_t)(strtoul(slash + 1, NULL, 10) & 0xff); + if (errno) { + errno = 0; + return -1; + } + + return 0; + } + + tmp = netlink_message_parse(buffer, len, "DEVPATH"); + if (!tmp) + return -1; + + slash = strrchr(tmp, '/'); + if (slash) + *sys_name = slash + 1; + + /* found a usb device */ + return 0; +} + +static int linux_netlink_read_message(void) +{ + char cred_buffer[CMSG_SPACE(sizeof(struct ucred))]; + char msg_buffer[2048]; + const char *sys_name = NULL; + uint8_t busnum, devaddr; + int detached, r; + ssize_t len; + struct cmsghdr *cmsg; + struct ucred *cred; + struct sockaddr_nl sa_nl; + struct iovec iov = { .iov_base = msg_buffer, .iov_len = sizeof(msg_buffer) }; + struct msghdr msg = { + .msg_iov = &iov, .msg_iovlen = 1, + .msg_control = cred_buffer, .msg_controllen = sizeof(cred_buffer), + .msg_name = &sa_nl, .msg_namelen = sizeof(sa_nl) + }; + + /* read netlink message */ + len = recvmsg(linux_netlink_socket, &msg, 0); + if (len == -1) { + if (errno != EAGAIN && errno != EINTR) + usbi_err(NULL, "error receiving message from netlink, errno=%d", errno); + return -1; + } + + if (len < 32 || (msg.msg_flags & MSG_TRUNC)) { + usbi_err(NULL, "invalid netlink message length"); + return -1; + } + + if (sa_nl.nl_groups != NL_GROUP_KERNEL || sa_nl.nl_pid != 0) { + usbi_dbg(NULL, "ignoring netlink message from unknown group/PID (%u/%u)", + (unsigned int)sa_nl.nl_groups, (unsigned int)sa_nl.nl_pid); + return -1; + } + + cmsg = CMSG_FIRSTHDR(&msg); + if (!cmsg || cmsg->cmsg_type != SCM_CREDENTIALS) { + usbi_dbg(NULL, "ignoring netlink message with no sender credentials"); + return -1; + } + + cred = (struct ucred *)CMSG_DATA(cmsg); + if (cred->uid != 0) { + usbi_dbg(NULL, "ignoring netlink message with non-zero sender UID %u", (unsigned int)cred->uid); + return -1; + } + + r = linux_netlink_parse(msg_buffer, (size_t)len, &detached, &sys_name, &busnum, &devaddr); + if (r) + return r; + + usbi_dbg(NULL, "netlink hotplug found device busnum: %hhu, devaddr: %hhu, sys_name: %s, removed: %s", + busnum, devaddr, sys_name, detached ? "yes" : "no"); + + /* signal device is available (or not) to all contexts */ + if (detached) + linux_device_disconnected(busnum, devaddr); + else + linux_hotplug_enumerate(busnum, devaddr, sys_name); + + return 0; +} + +static void *linux_netlink_event_thread_main(void *arg) +{ + struct pollfd fds[] = { + { .fd = USBI_EVENT_OS_HANDLE(&netlink_control_event), + .events = USBI_EVENT_POLL_EVENTS }, + { .fd = linux_netlink_socket, + .events = POLLIN }, + }; + int r; + + UNUSED(arg); + +#if defined(HAVE_PTHREAD_SETNAME_NP) + r = pthread_setname_np(pthread_self(), "libusb_event"); + if (r) + usbi_warn(NULL, "failed to set hotplug event thread name, error=%d", r); +#endif + + usbi_dbg(NULL, "netlink event thread entering"); + + while (1) { + r = poll(fds, 2, -1); + if (r == -1) { + /* check for temporary failure */ + if (errno == EINTR) + continue; + usbi_err(NULL, "poll() failed, errno=%d", errno); + break; + } + if (fds[0].revents) { + /* activity on control event, exit */ + break; + } + if (fds[1].revents) { + usbi_mutex_static_lock(&linux_hotplug_lock); + linux_netlink_read_message(); + usbi_mutex_static_unlock(&linux_hotplug_lock); + } + } + + usbi_dbg(NULL, "netlink event thread exiting"); + + return NULL; +} + +void linux_netlink_hotplug_poll(void) +{ + int r; + + usbi_mutex_static_lock(&linux_hotplug_lock); + do { + r = linux_netlink_read_message(); + } while (r == 0); + usbi_mutex_static_unlock(&linux_hotplug_lock); +} diff --git a/usbaudio/src/main/cpp/external/libusb/libusb/os/linux_usbfs.c b/usbaudio/src/main/cpp/external/libusb/libusb/os/linux_usbfs.c new file mode 100644 index 000000000..74e73f9a8 --- /dev/null +++ b/usbaudio/src/main/cpp/external/libusb/libusb/os/linux_usbfs.c @@ -0,0 +1,2877 @@ +/* -*- Mode: C; c-basic-offset:8 ; indent-tabs-mode:t -*- */ +/* + * Linux usbfs backend for libusb + * Copyright © 2007-2009 Daniel Drake + * Copyright © 2001 Johannes Erdfelt + * Copyright © 2013 Nathan Hjelm + * Copyright © 2012-2013 Hans de Goede + * Copyright © 2020 Chris Dickens + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "libusbi.h" +#include "linux_usbfs.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* sysfs vs usbfs: + * opening a usbfs node causes the device to be resumed, so we attempt to + * avoid this during enumeration. + * + * sysfs allows us to read the kernel's in-memory copies of device descriptors + * and so forth, avoiding the need to open the device: + * - The binary "descriptors" file contains all config descriptors since + * 2.6.26, commit 217a9081d8e69026186067711131b77f0ce219ed + * - The binary "descriptors" file was added in 2.6.23, commit + * 69d42a78f935d19384d1f6e4f94b65bb162b36df, but it only contains the + * active config descriptors + * - The "busnum" file was added in 2.6.22, commit + * 83f7d958eab2fbc6b159ee92bf1493924e1d0f72 + * - The "devnum" file has been present since pre-2.6.18 + * - the "bConfigurationValue" file has been present since pre-2.6.18 + * + * If we have bConfigurationValue, busnum, and devnum, then we can determine + * the active configuration without having to open the usbfs node in RDWR mode. + * The busnum file is important as that is the only way we can relate sysfs + * devices to usbfs nodes. + * + * If we also have all descriptors, we can obtain the device descriptor and + * configuration without touching usbfs at all. + */ + +/* endianness for multi-byte fields: + * + * Descriptors exposed by usbfs have the multi-byte fields in the device + * descriptor as host endian. Multi-byte fields in the other descriptors are + * bus-endian. The kernel documentation says otherwise, but it is wrong. + * + * In sysfs all descriptors are bus-endian. + */ + +#define USBDEV_PATH "/dev" +#define USB_DEVTMPFS_PATH "/dev/bus/usb" + +/* use usbdev*.* device names in /dev instead of the usbfs bus directories */ +static int usbdev_names = 0; + +/* Linux has changed the maximum length of an individual isochronous packet + * over time. Initially this limit was 1,023 bytes, but Linux 2.6.18 + * (commit 3612242e527eb47ee4756b5350f8bdf791aa5ede) increased this value to + * 8,192 bytes to support higher bandwidth devices. Linux 3.10 + * (commit e2e2f0ea1c935edcf53feb4c4c8fdb4f86d57dd9) further increased this + * value to 49,152 bytes to support super speed devices. Linux 5.2 + * (commit 8a1dbc8d91d3d1602282c7e6b4222c7759c916fa) even further increased + * this value to 98,304 bytes to support super speed plus devices. + */ +static unsigned int max_iso_packet_len = 0; + +/* is sysfs available (mounted) ? */ +static int sysfs_available = -1; + +/* how many times have we initted (and not exited) ? */ +static int init_count = 0; + +/* Serialize scan-devices, event-thread, and poll */ +usbi_mutex_static_t linux_hotplug_lock = USBI_MUTEX_INITIALIZER; + +static int open_sysfs_attr(struct libusb_context *ctx, + const char *sysfs_dir, const char *attr); +static int linux_scan_devices(struct libusb_context *ctx); +static int detach_kernel_driver_and_claim(struct libusb_device_handle *, uint8_t); + +#if !defined(HAVE_LIBUDEV) +static int linux_default_scan_devices(struct libusb_context *ctx); +#endif + +struct kernel_version { + int major; + int minor; + int sublevel; +}; + +struct config_descriptor { + struct usbi_configuration_descriptor *desc; + size_t actual_len; +}; + +struct linux_context_priv { + /* no enumeration or hot-plug detection */ + int no_device_discovery; +}; + +struct linux_device_priv { + char *sysfs_dir; + void *descriptors; + size_t descriptors_len; + struct config_descriptor *config_descriptors; + int active_config; /* cache val for !sysfs_available */ +}; + +struct linux_device_handle_priv { + int fd; + int fd_removed; + int fd_keep; + uint32_t caps; +}; + +enum reap_action { + NORMAL = 0, + /* submission failed after the first URB, so await cancellation/completion + * of all the others */ + SUBMIT_FAILED, + + /* cancelled by user or timeout */ + CANCELLED, + + /* completed multi-URB transfer in non-final URB */ + COMPLETED_EARLY, + + /* one or more urbs encountered a low-level error */ + ERROR, +}; + +struct linux_transfer_priv { + union { + struct usbfs_urb *urbs; + struct usbfs_urb **iso_urbs; + }; + + enum reap_action reap_action; + int num_urbs; + int num_retired; + enum libusb_transfer_status reap_status; + + /* next iso packet in user-supplied transfer to be populated */ + int iso_packet_offset; +}; + +static int dev_has_config0(struct libusb_device *dev) +{ + struct linux_device_priv *priv = usbi_get_device_priv(dev); + struct config_descriptor *config; + uint8_t idx; + + for (idx = 0; idx < dev->device_descriptor.bNumConfigurations; idx++) { + config = &priv->config_descriptors[idx]; + if (config->desc->bConfigurationValue == 0) + return 1; + } + + return 0; +} + +static int get_usbfs_fd(struct libusb_device *dev, int access_mode, int silent) +{ + struct libusb_context *ctx = DEVICE_CTX(dev); + char path[24]; + int fd; + + if (usbdev_names) + snprintf(path, sizeof(path), USBDEV_PATH "/usbdev%u.%u", + dev->bus_number, dev->device_address); + else + snprintf(path, sizeof(path), USB_DEVTMPFS_PATH "/%03u/%03u", + dev->bus_number, dev->device_address); + + fd = open(path, access_mode | O_CLOEXEC); + if (fd != -1) + return fd; /* Success */ + + /* TODO: fix race between netlink and usbfs https://github.com/libusb/libusb/issues/1691 */ + if (errno == ENOENT) { + const long delay_ms = 10L; + const struct timespec delay_ts = { 0L, delay_ms * 1000L * 1000L }; + uint8_t retry = 3; + + while (retry-- > 0) { + if (!silent) + usbi_err(ctx, "File doesn't exist, wait %ld ms and try again", delay_ms); + + /* Wait 10ms for USB device path creation.*/ + nanosleep(&delay_ts, NULL); + + fd = open(path, access_mode | O_CLOEXEC); + if (fd != -1) + return fd; /* Success */ + if (errno != ENOENT) + break; + } + } + + if (!silent) { + usbi_err(ctx, "libusb couldn't open USB device %s, errno=%d", path, errno); + if (errno == EACCES && access_mode == O_RDWR) + usbi_err(ctx, "libusb requires write access to USB device nodes"); + } + + if (errno == EACCES) + return LIBUSB_ERROR_ACCESS; + if (errno == ENOENT) + return LIBUSB_ERROR_NO_DEVICE; + return LIBUSB_ERROR_IO; +} + +/* check dirent for a /dev/usbdev%d.%d name + * optionally return bus/device on success */ +static int is_usbdev_entry(const char *name, uint8_t *bus_p, uint8_t *dev_p) +{ + int busnum, devnum; + + if (sscanf(name, "usbdev%d.%d", &busnum, &devnum) != 2) + return 0; + if (busnum < 0 || busnum > UINT8_MAX || devnum < 0 || devnum > UINT8_MAX) { + usbi_dbg(NULL, "invalid usbdev format '%s'", name); + return 0; + } + + usbi_dbg(NULL, "found: %s", name); + if (bus_p) + *bus_p = (uint8_t)busnum; + if (dev_p) + *dev_p = (uint8_t)devnum; + return 1; +} + +static const char *find_usbfs_path(void) +{ + const char *path; + DIR *dir; + struct dirent *entry; + + path = USB_DEVTMPFS_PATH; + dir = opendir(path); + if (dir) { + while ((entry = readdir(dir))) { + if (entry->d_name[0] == '.') + continue; + + /* We assume if we find any files that it must be the right place */ + break; + } + + closedir(dir); + + if (entry) + return path; + } + + /* look for /dev/usbdev*.* if the normal place fails */ + path = USBDEV_PATH; + dir = opendir(path); + if (dir) { + while ((entry = readdir(dir))) { + if (entry->d_name[0] == '.') + continue; + + if (is_usbdev_entry(entry->d_name, NULL, NULL)) { + /* found one; that's enough */ + break; + } + } + + closedir(dir); + + if (entry) { + usbdev_names = 1; + return path; + } + } + + return NULL; +} + +static int get_kernel_version(struct libusb_context *ctx, + struct kernel_version *ver) +{ + struct utsname uts; + int atoms; + + if (uname(&uts) < 0) { + usbi_err(ctx, "uname failed, errno=%d", errno); + return -1; + } + + atoms = sscanf(uts.release, "%d.%d.%d", &ver->major, &ver->minor, &ver->sublevel); + if (atoms < 2) { + usbi_err(ctx, "failed to parse uname release '%s'", uts.release); + return -1; + } + + if (atoms < 3) + ver->sublevel = -1; + + usbi_dbg(ctx, "reported kernel version is %s", uts.release); + + return 0; +} + +static int kernel_version_ge(const struct kernel_version *ver, + int major, int minor, int sublevel) +{ + if (ver->major > major) + return 1; + else if (ver->major < major) + return 0; + + /* kmajor == major */ + if (ver->minor > minor) + return 1; + else if (ver->minor < minor) + return 0; + + /* kminor == minor */ + if (ver->sublevel == -1) + return sublevel == 0; + + return ver->sublevel >= sublevel; +} + +static int op_init(struct libusb_context *ctx) +{ + struct kernel_version kversion; + const char *usbfs_path; + int r; + struct linux_context_priv *cpriv = usbi_get_context_priv(ctx); + + if (get_kernel_version(ctx, &kversion) < 0) + return LIBUSB_ERROR_OTHER; + + if (!kernel_version_ge(&kversion, 2, 6, 32)) { + usbi_err(ctx, "kernel version is too old (reported as %d.%d.%d)", + kversion.major, kversion.minor, + kversion.sublevel != -1 ? kversion.sublevel : 0); + return LIBUSB_ERROR_NOT_SUPPORTED; + } + + usbfs_path = find_usbfs_path(); + if (!usbfs_path) { + /* On udev based systems without any usb devices /dev/bus/usb will not + * exist. On android and other systems SELinux policies might block us + * from reading /dev. Nonetheless it's the default for most modern linux + systems including those using udev alternatives such as busybox's mdev. */ + usbfs_path = USB_DEVTMPFS_PATH; + usbi_dbg(ctx, "could not find usbfs, defaulting to %s", usbfs_path); + } else { + usbi_dbg(ctx, "found usbfs at %s", usbfs_path); + } + + if (!max_iso_packet_len) { + if (kernel_version_ge(&kversion, 5, 2, 0)) + max_iso_packet_len = 98304; + else if (kernel_version_ge(&kversion, 3, 10, 0)) + max_iso_packet_len = 49152; + else + max_iso_packet_len = 8192; + } + + usbi_dbg(ctx, "max iso packet length is (likely) %u bytes", max_iso_packet_len); + + if (sysfs_available == -1) { + struct statfs statfsbuf; + + r = statfs(SYSFS_MOUNT_PATH, &statfsbuf); + if (r == 0 && statfsbuf.f_type == SYSFS_MAGIC) { + usbi_dbg(ctx, "sysfs is available"); + sysfs_available = 1; + } else { + usbi_warn(ctx, "sysfs not mounted"); + sysfs_available = 0; + } + } + + if (cpriv->no_device_discovery) { + return LIBUSB_SUCCESS; + } + + r = LIBUSB_SUCCESS; + if (init_count == 0) { + /* start up hotplug event handler */ + r = linux_start_event_monitor(); + } + if (r == LIBUSB_SUCCESS) { + r = linux_scan_devices(ctx); + if (r == LIBUSB_SUCCESS) + init_count++; + else if (init_count == 0) + linux_stop_event_monitor(); + } else { + usbi_err(ctx, "error starting hotplug event monitor"); + } + + return r; +} + +static void op_exit(struct libusb_context *ctx) +{ + struct linux_context_priv *cpriv = usbi_get_context_priv(ctx); + + if (cpriv->no_device_discovery) { + return; + } + + assert(init_count != 0); + if (!--init_count) { + /* tear down event handler */ + linux_stop_event_monitor(); + } +} + +static int op_set_option(struct libusb_context *ctx, enum libusb_option option, va_list ap) +{ + UNUSED(ap); + + if (option == LIBUSB_OPTION_NO_DEVICE_DISCOVERY) { + struct linux_context_priv *cpriv = usbi_get_context_priv(ctx); + + usbi_dbg(ctx, "no device discovery will be performed"); + cpriv->no_device_discovery = 1; + return LIBUSB_SUCCESS; + } + + return LIBUSB_ERROR_NOT_SUPPORTED; +} + +static int op_get_device_string(struct libusb_device *dev, + enum libusb_device_string_type string_type, char *buffer, int length) +{ + ssize_t r; + int fd; + struct linux_device_priv *priv = usbi_get_device_priv(dev); + struct libusb_context* ctx = DEVICE_CTX(dev); + const char * attr; + + switch (string_type) { + case LIBUSB_DEVICE_STRING_MANUFACTURER: attr = "manufacturer"; break; + case LIBUSB_DEVICE_STRING_PRODUCT: attr = "product"; break; + case LIBUSB_DEVICE_STRING_SERIAL_NUMBER: attr = "serial"; break; + case LIBUSB_DEVICE_STRING_COUNT: + /* intentional fall-through, avoid -Wswitch-enum */ + default: + return LIBUSB_ERROR_INVALID_PARAM; + } + fd = open_sysfs_attr(ctx, priv->sysfs_dir, attr); + if (fd < 0) + return LIBUSB_ERROR_IO; + + r = read(fd, buffer, length - 1); // leave space for null terminator + if (r < 0) { + r = errno; + close(fd); + if (r == ENODEV) + return LIBUSB_ERROR_NO_DEVICE; + usbi_err(ctx, "attribute %s read failed, errno=%zd", attr, r); + return LIBUSB_ERROR_IO; + } + close(fd); + buffer[r] = 0; // add null terminator + while (r && ((buffer[r] == 0) || (buffer[r] == '\n'))) { + buffer[r--] = 0; + } + ++r; + return r; +} + +static int linux_scan_devices(struct libusb_context *ctx) +{ + int ret; + + usbi_mutex_static_lock(&linux_hotplug_lock); + +#if defined(HAVE_LIBUDEV) + ret = linux_udev_scan_devices(ctx); +#else + ret = linux_default_scan_devices(ctx); +#endif + + usbi_mutex_static_unlock(&linux_hotplug_lock); + + return ret; +} + +static void op_hotplug_poll(void) +{ + linux_hotplug_poll(); +} + +static int open_sysfs_attr(struct libusb_context *ctx, + const char *sysfs_dir, const char *attr) +{ + char filename[256]; + int fd; + + snprintf(filename, sizeof(filename), SYSFS_DEVICE_PATH "/%s/%s", sysfs_dir, attr); + fd = open(filename, O_RDONLY | O_CLOEXEC); + if (fd < 0) { + if (errno == ENOENT) { + /* File doesn't exist. Assume the device has been + disconnected (see trac ticket #70). */ + return LIBUSB_ERROR_NO_DEVICE; + } + usbi_err(ctx, "open %s failed, errno=%d", filename, errno); + return LIBUSB_ERROR_IO; + } + + return fd; +} + +/* Note only suitable for attributes which always read >= 0, < 0 is error */ +static int read_sysfs_attr(struct libusb_context *ctx, + const char *sysfs_dir, const char *attr, int max_value, int *value_p) +{ + char buf[20], *endptr; + long value; + ssize_t r; + int fd; + + fd = open_sysfs_attr(ctx, sysfs_dir, attr); + if (fd < 0) + return fd; + + r = read(fd, buf, sizeof(buf) - 1); + if (r < 0) { + r = errno; + close(fd); + if (r == ENODEV) + return LIBUSB_ERROR_NO_DEVICE; + usbi_err(ctx, "attribute %s read failed, errno=%zd", attr, r); + return LIBUSB_ERROR_IO; + } + close(fd); + + if (r == 0) { + /* Certain attributes (e.g. bConfigurationValue) are not + * populated if the device is not configured. */ + *value_p = -1; + return 0; + } + + /* The kernel does *not* NUL-terminate the string, but every attribute + * should be terminated with a newline character. */ + if (!isdigit(buf[0])) { + usbi_err(ctx, "attribute %s doesn't have numeric value?", attr); + return LIBUSB_ERROR_IO; + } else if (buf[r - 1] != '\n') { + usbi_warn(ctx, "attribute %s doesn't end with newline?", attr); + } else { + /* Remove the terminating newline character */ + r--; + } + buf[r] = '\0'; + + errno = 0; + value = strtol(buf, &endptr, 10); + if (buf == endptr || value < 0 || value > (long)max_value || errno) { + usbi_err(ctx, "attribute %s contains an invalid value: '%s'", attr, buf); + return LIBUSB_ERROR_INVALID_PARAM; + } else if (*endptr != '\0') { + /* Consider the value to be valid if the remainder is a '.' + * character followed by numbers. This occurs, for example, + * when reading the "speed" attribute for a low-speed device + * (e.g. "1.5") */ + if (*endptr == '.' && isdigit(*(endptr + 1))) { + endptr++; + while (isdigit(*endptr)) + endptr++; + } + if (*endptr != '\0') { + usbi_err(ctx, "attribute %s contains an invalid value: '%s'", attr, buf); + return LIBUSB_ERROR_INVALID_PARAM; + } + } + + *value_p = (int)value; + return 0; +} + +static int sysfs_scan_device(struct libusb_context *ctx, const char *devname) +{ + uint8_t busnum, devaddr; + int ret; + + ret = linux_get_device_address(ctx, 0, &busnum, &devaddr, NULL, devname, -1); + if (ret != LIBUSB_SUCCESS) + return ret; + + return linux_enumerate_device(ctx, busnum, devaddr, devname); +} + +/* read the bConfigurationValue for a device */ +static int sysfs_get_active_config(struct libusb_device *dev, int *config) +{ + struct linux_device_priv *priv = usbi_get_device_priv(dev); + + return read_sysfs_attr(DEVICE_CTX(dev), priv->sysfs_dir, "bConfigurationValue", + UINT8_MAX, config); +} + +int linux_get_device_address(struct libusb_context *ctx, int detached, + uint8_t *busnum, uint8_t *devaddr, const char *dev_node, + const char *sys_name, int fd) +{ + int sysfs_val; + int r; + + usbi_dbg(ctx, "getting address for device: %s detached: %d", sys_name, detached); + /* can't use sysfs to read the bus and device number if the + * device has been detached */ + if (!sysfs_available || detached || !sys_name) { + if (!dev_node && fd >= 0) { + char *fd_path = alloca(PATH_MAX); + char proc_path[32]; + + /* try to retrieve the device node from fd */ + snprintf(proc_path, sizeof(proc_path), "/proc/self/fd/%d", fd); + r = readlink(proc_path, fd_path, PATH_MAX - 1); + if (r > 0) { + fd_path[r] = '\0'; + dev_node = fd_path; + } + } + + if (!dev_node) + return LIBUSB_ERROR_OTHER; + + /* will this work with all supported kernel versions? */ + if (!strncmp(dev_node, "/dev/bus/usb", 12)) + sscanf(dev_node, "/dev/bus/usb/%hhu/%hhu", busnum, devaddr); + else + return LIBUSB_ERROR_OTHER; + + return LIBUSB_SUCCESS; + } + + usbi_dbg(ctx, "scan %s", sys_name); + + r = read_sysfs_attr(ctx, sys_name, "busnum", UINT8_MAX, &sysfs_val); + if (r < 0) + return r; + *busnum = (uint8_t)sysfs_val; + + r = read_sysfs_attr(ctx, sys_name, "devnum", UINT8_MAX, &sysfs_val); + if (r < 0) + return r; + *devaddr = (uint8_t)sysfs_val; + + usbi_dbg(ctx, "bus=%u dev=%u", *busnum, *devaddr); + + return LIBUSB_SUCCESS; +} + +/* Return offset of the next config descriptor */ +static int seek_to_next_config(struct libusb_context *ctx, + uint8_t *buffer, size_t len) +{ + struct usbi_descriptor_header *header; + int offset; + + /* Start seeking past the config descriptor */ + offset = LIBUSB_DT_CONFIG_SIZE; + buffer += LIBUSB_DT_CONFIG_SIZE; + len -= LIBUSB_DT_CONFIG_SIZE; + + while (len > 0) { + if (len < 2) { + usbi_err(ctx, "remaining descriptor length too small %zu/2", len); + return LIBUSB_ERROR_IO; + } + + header = (struct usbi_descriptor_header *)buffer; + if (header->bDescriptorType == LIBUSB_DT_CONFIG) + return offset; + + if (header->bLength < 2) { + usbi_err(ctx, "invalid descriptor bLength %hhu", header->bLength); + return LIBUSB_ERROR_IO; + } + + if (len < header->bLength) { + usbi_err(ctx, "bLength overflow by %zu bytes", + (size_t)header->bLength - len); + return LIBUSB_ERROR_IO; + } + + offset += header->bLength; + buffer += header->bLength; + len -= header->bLength; + } + + usbi_err(ctx, "config descriptor not found"); + return LIBUSB_ERROR_IO; +} + +static int parse_config_descriptors(struct libusb_device *dev) +{ + struct libusb_context *ctx = DEVICE_CTX(dev); + struct linux_device_priv *priv = usbi_get_device_priv(dev); + struct usbi_device_descriptor *device_desc; + uint8_t idx, num_configs; + uint8_t *buffer; + size_t remaining; + + device_desc = priv->descriptors; + num_configs = device_desc->bNumConfigurations; + + if (num_configs == 0) + return 0; /* no configurations? */ + + priv->config_descriptors = malloc(num_configs * sizeof(priv->config_descriptors[0])); + if (!priv->config_descriptors) + return LIBUSB_ERROR_NO_MEM; + + buffer = (uint8_t *)priv->descriptors + LIBUSB_DT_DEVICE_SIZE; + remaining = priv->descriptors_len - LIBUSB_DT_DEVICE_SIZE; + + for (idx = 0; idx < num_configs; idx++) { + struct usbi_configuration_descriptor *config_desc; + uint16_t config_len; + + if (remaining < LIBUSB_DT_CONFIG_SIZE) { + usbi_err(ctx, "short descriptor read %zu/%d", + remaining, LIBUSB_DT_CONFIG_SIZE); + return LIBUSB_ERROR_IO; + } + + config_desc = (struct usbi_configuration_descriptor *)buffer; + if (config_desc->bDescriptorType != LIBUSB_DT_CONFIG) { + usbi_err(ctx, "descriptor is not a config desc (type 0x%02x)", + config_desc->bDescriptorType); + return LIBUSB_ERROR_IO; + } else if (config_desc->bLength < LIBUSB_DT_CONFIG_SIZE) { + usbi_err(ctx, "invalid descriptor bLength %u", + config_desc->bLength); + return LIBUSB_ERROR_IO; + } + + config_len = libusb_le16_to_cpu(config_desc->wTotalLength); + if (config_len < LIBUSB_DT_CONFIG_SIZE) { + usbi_err(ctx, "invalid wTotalLength %u", config_len); + return LIBUSB_ERROR_IO; + } + + if (priv->sysfs_dir) { + /* + * In sysfs wTotalLength is ignored, instead the kernel returns a + * config descriptor with verified bLength fields, with descriptors + * with an invalid bLength removed. + */ + uint16_t sysfs_config_len; + int offset; + + if (num_configs > 1 && idx < num_configs - 1) { + offset = seek_to_next_config(ctx, buffer, remaining); + if (offset < 0) + return offset; + sysfs_config_len = (uint16_t)offset; + } else { + sysfs_config_len = (uint16_t)remaining; + } + + if (config_len != sysfs_config_len) { + usbi_warn(ctx, "config length mismatch wTotalLength %u real %u", + config_len, sysfs_config_len); + config_len = sysfs_config_len; + } + } else { + /* + * In usbfs the config descriptors are wTotalLength bytes apart, + * with any short reads from the device appearing as holes in the file. + */ + if (config_len > remaining) { + usbi_warn(ctx, "short descriptor read %zu/%u", remaining, config_len); + config_len = (uint16_t)remaining; + } + } + + if (config_desc->bConfigurationValue == 0) + usbi_warn(ctx, "device has configuration 0"); + + priv->config_descriptors[idx].desc = config_desc; + priv->config_descriptors[idx].actual_len = config_len; + + buffer += config_len; + remaining -= config_len; + } + + return LIBUSB_SUCCESS; +} + +static int op_get_config_descriptor_by_value(struct libusb_device *dev, + uint8_t value, void **buffer) +{ + struct linux_device_priv *priv = usbi_get_device_priv(dev); + struct config_descriptor *config; + uint8_t idx; + + for (idx = 0; idx < dev->device_descriptor.bNumConfigurations; idx++) { + config = &priv->config_descriptors[idx]; + if (config->desc->bConfigurationValue == value) { + *buffer = config->desc; + return (int)config->actual_len; + } + } + + return LIBUSB_ERROR_NOT_FOUND; +} + +static int op_get_active_config_descriptor(struct libusb_device *dev, + void *buffer, size_t len) +{ + struct linux_device_priv *priv = usbi_get_device_priv(dev); + void *config_desc; + int active_config; + int r; + + if (priv->sysfs_dir) { + r = sysfs_get_active_config(dev, &active_config); + if (r < 0) + return r; + } else { + /* Use cached bConfigurationValue */ + active_config = priv->active_config; + } + + if (active_config == -1) { + usbi_err(DEVICE_CTX(dev), "device unconfigured"); + return LIBUSB_ERROR_NOT_FOUND; + } + + r = op_get_config_descriptor_by_value(dev, (uint8_t)active_config, &config_desc); + if (r < 0) + return r; + + len = MIN(len, (size_t)r); + memcpy(buffer, config_desc, len); + return len; +} + +static int op_get_config_descriptor(struct libusb_device *dev, + uint8_t config_index, void *buffer, size_t len) +{ + struct linux_device_priv *priv = usbi_get_device_priv(dev); + struct config_descriptor *config; + + if (config_index >= dev->device_descriptor.bNumConfigurations) + return LIBUSB_ERROR_NOT_FOUND; + + config = &priv->config_descriptors[config_index]; + len = MIN(len, config->actual_len); + memcpy(buffer, config->desc, len); + return len; +} + +/* send a control message to retrieve active configuration */ +static int usbfs_get_active_config(struct libusb_device *dev, int fd) +{ + struct linux_device_priv *priv = usbi_get_device_priv(dev); + uint8_t active_config = 0; + int r; + + struct usbfs_ctrltransfer ctrl = { + .bmRequestType = LIBUSB_ENDPOINT_IN, + .bRequest = LIBUSB_REQUEST_GET_CONFIGURATION, + .wValue = 0, + .wIndex = 0, + .wLength = 1, + .timeout = 1000, + .data = &active_config + }; + + r = ioctl(fd, IOCTL_USBFS_CONTROL, &ctrl); + if (r < 0) { + if (errno == ENODEV) + return LIBUSB_ERROR_NO_DEVICE; + + /* we hit this error path frequently with buggy devices :( */ + usbi_warn(DEVICE_CTX(dev), "get configuration failed, errno=%d", errno); + + /* assume the current configuration is the first one if we have + * the configuration descriptors, otherwise treat the device + * as unconfigured. */ + if (priv->config_descriptors) + priv->active_config = (int)priv->config_descriptors[0].desc->bConfigurationValue; + else + priv->active_config = -1; + } else if (active_config == 0) { + if (dev_has_config0(dev)) { + /* some buggy devices have a configuration 0, but we're + * reaching into the corner of a corner case here. */ + priv->active_config = 0; + } else { + priv->active_config = -1; + } + } else { + priv->active_config = (int)active_config; + } + + return LIBUSB_SUCCESS; +} + +static enum libusb_speed usbfs_get_speed(struct libusb_context *ctx, int fd) +{ + int r; + + r = ioctl(fd, IOCTL_USBFS_GET_SPEED, NULL); + switch (r) { + case USBFS_SPEED_UNKNOWN: return LIBUSB_SPEED_UNKNOWN; + case USBFS_SPEED_LOW: return LIBUSB_SPEED_LOW; + case USBFS_SPEED_FULL: return LIBUSB_SPEED_FULL; + case USBFS_SPEED_HIGH: return LIBUSB_SPEED_HIGH; + case USBFS_SPEED_WIRELESS: return LIBUSB_SPEED_HIGH; + case USBFS_SPEED_SUPER: return LIBUSB_SPEED_SUPER; + case USBFS_SPEED_SUPER_PLUS: return LIBUSB_SPEED_SUPER_PLUS; + default: + usbi_warn(ctx, "Error getting device speed: %d", r); + } + + return LIBUSB_SPEED_UNKNOWN; +} + +static int initialize_device(struct libusb_device *dev, uint8_t busnum, + uint8_t devaddr, const char *sysfs_dir, int wrapped_fd) +{ + struct linux_device_priv *priv = usbi_get_device_priv(dev); + struct libusb_context *ctx = DEVICE_CTX(dev); + size_t alloc_len; + int fd, speed, r; + ssize_t nb; + + dev->bus_number = busnum; + dev->device_address = devaddr; + + if (sysfs_dir) { + priv->sysfs_dir = strdup(sysfs_dir); + if (!priv->sysfs_dir) + return LIBUSB_ERROR_NO_MEM; + + /* Note speed can contain 1.5, in this case read_sysfs_attr() + will stop parsing at the '.' and return 1 */ + if (read_sysfs_attr(ctx, sysfs_dir, "speed", INT_MAX, &speed) == 0) { + switch (speed) { + case 1: dev->speed = LIBUSB_SPEED_LOW; break; + case 12: dev->speed = LIBUSB_SPEED_FULL; break; + case 480: dev->speed = LIBUSB_SPEED_HIGH; break; + case 5000: dev->speed = LIBUSB_SPEED_SUPER; break; + case 10000: dev->speed = LIBUSB_SPEED_SUPER_PLUS; break; + case 20000: dev->speed = LIBUSB_SPEED_SUPER_PLUS_X2; break; + default: + usbi_warn(ctx, "unknown device speed: %d Mbps", speed); + } + } + } else if (wrapped_fd >= 0) { + dev->speed = usbfs_get_speed(ctx, wrapped_fd); + } + + /* cache descriptors in memory */ + if (sysfs_dir) { + fd = open_sysfs_attr(ctx, sysfs_dir, "descriptors"); + } else if (wrapped_fd < 0) { + fd = get_usbfs_fd(dev, O_RDONLY, 0); + } else { + fd = wrapped_fd; + r = lseek(fd, 0, SEEK_SET); + if (r < 0) { + usbi_err(ctx, "lseek failed, errno=%d", errno); + return LIBUSB_ERROR_IO; + } + } + if (fd < 0) + return fd; + + alloc_len = 0; + do { + const size_t desc_read_length = 256; + uint8_t *read_ptr; + + alloc_len += desc_read_length; + priv->descriptors = usbi_reallocf(priv->descriptors, alloc_len); + if (!priv->descriptors) { + if (fd != wrapped_fd) + close(fd); + return LIBUSB_ERROR_NO_MEM; + } + read_ptr = (uint8_t *)priv->descriptors + priv->descriptors_len; + /* usbfs has holes in the file */ + if (!sysfs_dir) + memset(read_ptr, 0, desc_read_length); + nb = read(fd, read_ptr, desc_read_length); + if (nb < 0) { + usbi_err(ctx, "read descriptor failed, errno=%d", errno); + if (fd != wrapped_fd) + close(fd); + return LIBUSB_ERROR_IO; + } + priv->descriptors_len += (size_t)nb; + } while (priv->descriptors_len == alloc_len); + + if (fd != wrapped_fd) + close(fd); + + if (priv->descriptors_len < LIBUSB_DT_DEVICE_SIZE) { + usbi_err(ctx, "short descriptor read (%zu)", priv->descriptors_len); + return LIBUSB_ERROR_IO; + } + + r = parse_config_descriptors(dev); + if (r < 0) + return r; + + memcpy(&dev->device_descriptor, priv->descriptors, LIBUSB_DT_DEVICE_SIZE); + + if (sysfs_dir) { + /* sysfs descriptors are in bus-endian format */ + usbi_localize_device_descriptor(&dev->device_descriptor); + return LIBUSB_SUCCESS; + } + + /* cache active config */ + if (wrapped_fd < 0) + fd = get_usbfs_fd(dev, O_RDWR, 1); + else + fd = wrapped_fd; + if (fd < 0) { + /* cannot send a control message to determine the active + * config. just assume the first one is active. */ + usbi_warn(ctx, "Missing rw usbfs access; cannot determine " + "active configuration descriptor"); + if (priv->config_descriptors) + priv->active_config = (int)priv->config_descriptors[0].desc->bConfigurationValue; + else + priv->active_config = -1; /* No config dt */ + + return LIBUSB_SUCCESS; + } + + r = usbfs_get_active_config(dev, fd); + if (fd != wrapped_fd) + close(fd); + + return r; +} + +static int linux_get_parent_info(struct libusb_device *dev, const char *sysfs_dir) +{ + struct libusb_context *ctx = DEVICE_CTX(dev); + struct libusb_device *it; + char *parent_sysfs_dir, *tmp, *end; + int ret, add_parent = 1; + + /* XXX -- can we figure out the topology when using usbfs? */ + if (!sysfs_dir || !strncmp(sysfs_dir, "usb", 3)) { + /* either using usbfs or finding the parent of a root hub */ + return LIBUSB_SUCCESS; + } + + parent_sysfs_dir = strdup(sysfs_dir); + if (!parent_sysfs_dir) + return LIBUSB_ERROR_NO_MEM; + + if ((tmp = strrchr(parent_sysfs_dir, '.')) || + (tmp = strrchr(parent_sysfs_dir, '-'))) { + const char *start = tmp + 1; + long port_number = strtol(start, &end, 10); + if (port_number < 0 || port_number > INT_MAX || start == end || '\0' != *end) { + usbi_warn(ctx, "Can not parse sysfs_dir: %s, unexpected parent info", + parent_sysfs_dir); + free(parent_sysfs_dir); + return LIBUSB_ERROR_OTHER; + } else { + dev->port_number = (int)port_number; + } + *tmp = '\0'; + } else { + usbi_warn(ctx, "Can not parse sysfs_dir: %s, no parent info", + parent_sysfs_dir); + free(parent_sysfs_dir); + return LIBUSB_SUCCESS; + } + + /* is the parent a root hub? */ + if (!strchr(parent_sysfs_dir, '-')) { + tmp = parent_sysfs_dir; + ret = asprintf(&parent_sysfs_dir, "usb%s", tmp); + free(tmp); + if (ret < 0) + return LIBUSB_ERROR_NO_MEM; + } + +retry: + /* find the parent in the context */ + usbi_mutex_lock(&ctx->usb_devs_lock); + for_each_device(ctx, it) { + struct linux_device_priv *priv = usbi_get_device_priv(it); + + if (priv->sysfs_dir) { + if (!strcmp(priv->sysfs_dir, parent_sysfs_dir)) { + dev->parent_dev = libusb_ref_device(it); + break; + } + } + } + usbi_mutex_unlock(&ctx->usb_devs_lock); + + if (!dev->parent_dev && add_parent) { + usbi_dbg(ctx, "parent_dev %s not enumerated yet, enumerating now", + parent_sysfs_dir); + sysfs_scan_device(ctx, parent_sysfs_dir); + add_parent = 0; + goto retry; + } + + usbi_dbg(ctx, "dev %p (%s) has parent %p (%s) port %u", + (void *) dev, sysfs_dir, (void *) dev->parent_dev, + parent_sysfs_dir, dev->port_number); + + free(parent_sysfs_dir); + + return LIBUSB_SUCCESS; +} + +int linux_enumerate_device(struct libusb_context *ctx, + uint8_t busnum, uint8_t devaddr, const char *sysfs_dir) +{ + unsigned long session_id; + struct libusb_device *dev; + int r; + + /* FIXME: session ID is not guaranteed unique as addresses can wrap and + * will be reused. instead we should add a simple sysfs attribute with + * a session ID. */ + session_id = busnum << 8 | devaddr; + usbi_dbg(ctx, "busnum %u devaddr %u session_id %lu", busnum, devaddr, session_id); + + dev = usbi_get_device_by_session_id(ctx, session_id); + if (dev) { + /* device already exists in the context */ + usbi_dbg(ctx, "session_id %lu already exists", session_id); + libusb_unref_device(dev); + return LIBUSB_SUCCESS; + } + + usbi_dbg(ctx, "allocating new device for %u/%u (session %lu)", + busnum, devaddr, session_id); + dev = usbi_alloc_device(ctx, session_id); + if (!dev) + return LIBUSB_ERROR_NO_MEM; + + r = initialize_device(dev, busnum, devaddr, sysfs_dir, -1); + if (r < 0) + goto out; + r = usbi_sanitize_device(dev); + if (r < 0) + goto out; + + r = linux_get_parent_info(dev, sysfs_dir); + if (r < 0) + goto out; +out: + if (r < 0) + libusb_unref_device(dev); + else + usbi_connect_device(dev); + + return r; +} + +void linux_hotplug_enumerate(uint8_t busnum, uint8_t devaddr, const char *sys_name) +{ + struct libusb_context *ctx; + + usbi_mutex_static_lock(&active_contexts_lock); + for_each_context(ctx) { + linux_enumerate_device(ctx, busnum, devaddr, sys_name); + } + usbi_mutex_static_unlock(&active_contexts_lock); +} + +void linux_device_disconnected(uint8_t busnum, uint8_t devaddr) +{ + struct libusb_context *ctx; + struct libusb_device *dev; + unsigned long session_id = busnum << 8 | devaddr; + + usbi_mutex_static_lock(&active_contexts_lock); + for_each_context(ctx) { + dev = usbi_get_device_by_session_id(ctx, session_id); + if (dev) { + usbi_disconnect_device(dev); + libusb_unref_device(dev); + } else { + usbi_dbg(ctx, "device not found for session %lx", session_id); + } + } + usbi_mutex_static_unlock(&active_contexts_lock); +} + +#if !defined(HAVE_LIBUDEV) +static int parse_u8(const char *str, uint8_t *val_p) +{ + char *endptr; + long num; + + errno = 0; + num = strtol(str, &endptr, 10); + if (num < 0 || num > UINT8_MAX || errno) + return 0; + if (endptr == str || *endptr != '\0') + return 0; + + *val_p = (uint8_t)num; + return 1; +} + +/* open a bus directory and adds all discovered devices to the context */ +static int usbfs_scan_busdir(struct libusb_context *ctx, uint8_t busnum) +{ + DIR *dir; + char dirpath[20]; + struct dirent *entry; + int r = LIBUSB_ERROR_IO; + + snprintf(dirpath, sizeof(dirpath), USB_DEVTMPFS_PATH "/%03u", busnum); + usbi_dbg(ctx, "%s", dirpath); + dir = opendir(dirpath); + if (!dir) { + usbi_err(ctx, "opendir '%s' failed, errno=%d", dirpath, errno); + /* FIXME: should handle valid race conditions like hub unplugged + * during directory iteration - this is not an error */ + return r; + } + + while ((entry = readdir(dir))) { + uint8_t devaddr; + + if (entry->d_name[0] == '.') + continue; + + if (!parse_u8(entry->d_name, &devaddr)) { + usbi_dbg(ctx, "unknown dir entry %s", entry->d_name); + continue; + } + + if (linux_enumerate_device(ctx, busnum, devaddr, NULL)) { + usbi_dbg(ctx, "failed to enumerate dir entry %s", entry->d_name); + continue; + } + + r = 0; + } + + closedir(dir); + return r; +} + +static int usbfs_get_device_list(struct libusb_context *ctx) +{ + struct dirent *entry; + DIR *buses; + uint8_t busnum, devaddr; + int r = 0; + + if (usbdev_names) + buses = opendir(USBDEV_PATH); + else + buses = opendir(USB_DEVTMPFS_PATH); + + if (!buses) { + if (!usbdev_names && errno == ENOENT) { + /* The path does not exist if there are no devices plugged in */ + return LIBUSB_SUCCESS; + } + usbi_err(ctx, "opendir buses failed, errno=%d", errno); + return LIBUSB_ERROR_IO; + } + + while ((entry = readdir(buses))) { + if (entry->d_name[0] == '.') + continue; + + if (usbdev_names) { + if (!is_usbdev_entry(entry->d_name, &busnum, &devaddr)) + continue; + + r = linux_enumerate_device(ctx, busnum, devaddr, NULL); + if (r < 0) { + usbi_dbg(ctx, "failed to enumerate dir entry %s", entry->d_name); + continue; + } + } else { + if (!parse_u8(entry->d_name, &busnum)) { + usbi_dbg(ctx, "unknown dir entry %s", entry->d_name); + continue; + } + + r = usbfs_scan_busdir(ctx, busnum); + if (r < 0) + break; + } + } + + closedir(buses); + return r; + +} + +static int sysfs_get_device_list(struct libusb_context *ctx) +{ + DIR *devices = opendir(SYSFS_DEVICE_PATH); + struct dirent *entry; + int num_devices = 0; + int num_enumerated = 0; + + if (!devices) { + usbi_err(ctx, "opendir devices failed, errno=%d", errno); + return LIBUSB_ERROR_IO; + } + + while ((entry = readdir(devices))) { + if ((!isdigit(entry->d_name[0]) && strncmp(entry->d_name, "usb", 3)) + || strchr(entry->d_name, ':')) + continue; + + num_devices++; + + if (sysfs_scan_device(ctx, entry->d_name)) { + usbi_dbg(ctx, "failed to enumerate dir entry %s", entry->d_name); + continue; + } + + num_enumerated++; + } + + closedir(devices); + + /* successful if at least one device was enumerated or no devices were found */ + if (num_enumerated || !num_devices) + return LIBUSB_SUCCESS; + else + return LIBUSB_ERROR_IO; +} + +static int linux_default_scan_devices(struct libusb_context *ctx) +{ + /* we can retrieve device list and descriptors from sysfs or usbfs. + * sysfs is preferable, because if we use usbfs we end up resuming + * any autosuspended USB devices. however, sysfs is not available + * everywhere, so we need a usbfs fallback too. + */ + if (sysfs_available && sysfs_get_device_list(ctx) == LIBUSB_SUCCESS) + return LIBUSB_SUCCESS; + + return usbfs_get_device_list(ctx); +} +#endif + +static int initialize_handle(struct libusb_device_handle *handle, int fd) +{ + struct linux_device_handle_priv *hpriv = usbi_get_device_handle_priv(handle); + int r; + + hpriv->fd = fd; + + r = ioctl(fd, IOCTL_USBFS_GET_CAPABILITIES, &hpriv->caps); + if (r < 0) { + if (errno == ENOTTY) + usbi_dbg(HANDLE_CTX(handle), "getcap not available"); + else + usbi_err(HANDLE_CTX(handle), "getcap failed, errno=%d", errno); + hpriv->caps = USBFS_CAP_BULK_CONTINUATION; + } + + return usbi_add_event_source(HANDLE_CTX(handle), hpriv->fd, POLLOUT); +} + +static int op_wrap_sys_device(struct libusb_context *ctx, + struct libusb_device_handle *handle, intptr_t sys_dev) +{ + struct linux_device_handle_priv *hpriv = usbi_get_device_handle_priv(handle); + int fd = (int)sys_dev; + uint8_t busnum, devaddr; + struct usbfs_connectinfo ci; + struct libusb_device *dev; + int r; + + r = linux_get_device_address(ctx, 1, &busnum, &devaddr, NULL, NULL, fd); + if (r < 0) { + r = ioctl(fd, IOCTL_USBFS_CONNECTINFO, &ci); + if (r < 0) { + usbi_err(ctx, "connectinfo failed, errno=%d", errno); + return LIBUSB_ERROR_IO; + } + /* There is no ioctl to get the bus number. We choose 0 here + * as linux starts numbering buses from 1. */ + busnum = 0; + devaddr = ci.devnum; + } + + /* Session id is unused as we do not add the device to the list of + * connected devices. */ + usbi_dbg(ctx, "allocating new device for fd %d", fd); + dev = usbi_alloc_device(ctx, 0); + if (!dev) + return LIBUSB_ERROR_NO_MEM; + + r = initialize_device(dev, busnum, devaddr, NULL, fd); + if (r < 0) + goto out; + r = usbi_sanitize_device(dev); + if (r < 0) + goto out; + /* Consider the device as connected, but do not add it to the managed + * device list. */ + usbi_atomic_store(&dev->attached, 1); + handle->dev = dev; + + r = initialize_handle(handle, fd); + hpriv->fd_keep = 1; + +out: + if (r < 0) + libusb_unref_device(dev); + return r; +} + +static int op_open(struct libusb_device_handle *handle) +{ + int fd, r; + + fd = get_usbfs_fd(handle->dev, O_RDWR, 0); + if (fd < 0) { + if (fd == LIBUSB_ERROR_NO_DEVICE) { + /* device will still be marked as attached if hotplug monitor thread + * hasn't processed remove event yet */ + usbi_mutex_static_lock(&linux_hotplug_lock); + if (usbi_atomic_load(&handle->dev->attached)) { + usbi_dbg(HANDLE_CTX(handle), "open failed with no device, but device still attached"); + linux_device_disconnected(handle->dev->bus_number, + handle->dev->device_address); + } + usbi_mutex_static_unlock(&linux_hotplug_lock); + } + return fd; + } + + r = initialize_handle(handle, fd); + if (r < 0) + close(fd); + + return r; +} + +static void op_close(struct libusb_device_handle *dev_handle) +{ + struct linux_device_handle_priv *hpriv = usbi_get_device_handle_priv(dev_handle); + + /* fd may have already been removed by POLLERR condition in op_handle_events() */ + if (!hpriv->fd_removed) + usbi_remove_event_source(HANDLE_CTX(dev_handle), hpriv->fd); + if (!hpriv->fd_keep) + close(hpriv->fd); +} + +static int op_get_configuration(struct libusb_device_handle *handle, + uint8_t *config) +{ + struct linux_device_priv *priv = usbi_get_device_priv(handle->dev); + int active_config = -1; /* to please compiler */ + int r; + + if (priv->sysfs_dir) { + r = sysfs_get_active_config(handle->dev, &active_config); + } else { + struct linux_device_handle_priv *hpriv = usbi_get_device_handle_priv(handle); + + r = usbfs_get_active_config(handle->dev, hpriv->fd); + if (r == LIBUSB_SUCCESS) + active_config = priv->active_config; + } + if (r < 0) + return r; + + if (active_config == -1) { + usbi_warn(HANDLE_CTX(handle), "device unconfigured"); + active_config = 0; + } + + *config = (uint8_t)active_config; + + return 0; +} + +static int op_set_configuration(struct libusb_device_handle *handle, int config) +{ + struct linux_device_priv *priv = usbi_get_device_priv(handle->dev); + struct linux_device_handle_priv *hpriv = usbi_get_device_handle_priv(handle); + int fd = hpriv->fd; + int r = ioctl(fd, IOCTL_USBFS_SETCONFIGURATION, &config); + + if (r < 0) { + if (errno == EINVAL) + return LIBUSB_ERROR_NOT_FOUND; + else if (errno == EBUSY) + return LIBUSB_ERROR_BUSY; + else if (errno == ENODEV) + return LIBUSB_ERROR_NO_DEVICE; + + usbi_err(HANDLE_CTX(handle), "set configuration failed, errno=%d", errno); + return LIBUSB_ERROR_OTHER; + } + + /* if necessary, update our cached active config descriptor */ + if (!priv->sysfs_dir) { + if (config == 0 && !dev_has_config0(handle->dev)) + config = -1; + + priv->active_config = config; + } + + return LIBUSB_SUCCESS; +} + +static int claim_interface(struct libusb_device_handle *handle, unsigned int iface) +{ + struct linux_device_handle_priv *hpriv = usbi_get_device_handle_priv(handle); + int fd = hpriv->fd; + int r = ioctl(fd, IOCTL_USBFS_CLAIMINTERFACE, &iface); + + if (r < 0) { + if (errno == ENOENT) + return LIBUSB_ERROR_NOT_FOUND; + else if (errno == EBUSY) + return LIBUSB_ERROR_BUSY; + else if (errno == ENODEV) + return LIBUSB_ERROR_NO_DEVICE; + + usbi_err(HANDLE_CTX(handle), "claim interface failed, errno=%d", errno); + return LIBUSB_ERROR_OTHER; + } + return 0; +} + +static int release_interface(struct libusb_device_handle *handle, unsigned int iface) +{ + struct linux_device_handle_priv *hpriv = usbi_get_device_handle_priv(handle); + int fd = hpriv->fd; + int r = ioctl(fd, IOCTL_USBFS_RELEASEINTERFACE, &iface); + + if (r < 0) { + if (errno == ENODEV) + return LIBUSB_ERROR_NO_DEVICE; + + usbi_err(HANDLE_CTX(handle), "release interface failed, errno=%d", errno); + return LIBUSB_ERROR_OTHER; + } + return 0; +} + +static int op_set_interface(struct libusb_device_handle *handle, uint8_t interface, + uint8_t altsetting) +{ + struct linux_device_handle_priv *hpriv = usbi_get_device_handle_priv(handle); + int fd = hpriv->fd; + struct usbfs_setinterface setintf; + int r; + + setintf.interface = interface; + setintf.altsetting = altsetting; + r = ioctl(fd, IOCTL_USBFS_SETINTERFACE, &setintf); + if (r < 0) { + if (errno == EINVAL) + return LIBUSB_ERROR_NOT_FOUND; + else if (errno == ENODEV) + return LIBUSB_ERROR_NO_DEVICE; + + usbi_err(HANDLE_CTX(handle), "set interface failed, errno=%d", errno); + return LIBUSB_ERROR_OTHER; + } + + return 0; +} + +static int op_clear_halt(struct libusb_device_handle *handle, + unsigned char endpoint) +{ + struct linux_device_handle_priv *hpriv = usbi_get_device_handle_priv(handle); + int fd = hpriv->fd; + unsigned int _endpoint = endpoint; + int r = ioctl(fd, IOCTL_USBFS_CLEAR_HALT, &_endpoint); + + if (r < 0) { + if (errno == ENOENT) + return LIBUSB_ERROR_NOT_FOUND; + else if (errno == ENODEV) + return LIBUSB_ERROR_NO_DEVICE; + + usbi_err(HANDLE_CTX(handle), "clear halt failed, errno=%d", errno); + return LIBUSB_ERROR_OTHER; + } + + return 0; +} + +static int op_reset_device(struct libusb_device_handle *handle) +{ + struct linux_device_handle_priv *hpriv = usbi_get_device_handle_priv(handle); + int fd = hpriv->fd; + int r, ret = 0; + uint8_t i; + + /* Doing a device reset will cause the usbfs driver to get unbound + * from any interfaces it is bound to. By voluntarily unbinding + * the usbfs driver ourself, we stop the kernel from rebinding + * the interface after reset (which would end up with the interface + * getting bound to the in kernel driver if any). */ + for (i = 0; i < USB_MAXINTERFACES; i++) { + if (handle->claimed_interfaces & (1UL << i)) + release_interface(handle, i); + } + + usbi_mutex_lock(&handle->lock); + r = ioctl(fd, IOCTL_USBFS_RESET, NULL); + if (r < 0) { + if (errno == ENODEV) { + ret = LIBUSB_ERROR_NOT_FOUND; + goto out; + } + + usbi_err(HANDLE_CTX(handle), "reset failed, errno=%d", errno); + ret = LIBUSB_ERROR_OTHER; + goto out; + } + + /* And re-claim any interfaces which were claimed before the reset */ + for (i = 0; i < USB_MAXINTERFACES; i++) { + if (!(handle->claimed_interfaces & (1UL << i))) + continue; + /* + * A driver may have completed modprobing during + * IOCTL_USBFS_RESET, and bound itself as soon as + * IOCTL_USBFS_RESET released the device lock + */ + r = detach_kernel_driver_and_claim(handle, i); + if (r) { + usbi_warn(HANDLE_CTX(handle), "failed to re-claim interface %u after reset: %s", + i, libusb_error_name(r)); + handle->claimed_interfaces &= ~(1UL << i); + ret = LIBUSB_ERROR_NOT_FOUND; + } + } +out: + usbi_mutex_unlock(&handle->lock); + return ret; +} + +static int do_streams_ioctl(struct libusb_device_handle *handle, + unsigned long req, uint32_t num_streams, unsigned char *endpoints, + int num_endpoints) +{ + struct linux_device_handle_priv *hpriv = usbi_get_device_handle_priv(handle); + int r, fd = hpriv->fd; + struct usbfs_streams *streams; + + if (num_endpoints > 30) /* Max 15 in + 15 out eps */ + return LIBUSB_ERROR_INVALID_PARAM; + + streams = malloc(sizeof(*streams) + num_endpoints); + if (!streams) + return LIBUSB_ERROR_NO_MEM; + + streams->num_streams = num_streams; + streams->num_eps = num_endpoints; + memcpy(streams->eps, endpoints, num_endpoints); + + r = ioctl(fd, req, streams); + + free(streams); + + if (r < 0) { + if (errno == ENOTTY) + return LIBUSB_ERROR_NOT_SUPPORTED; + else if (errno == EINVAL) + return LIBUSB_ERROR_INVALID_PARAM; + else if (errno == ENODEV) + return LIBUSB_ERROR_NO_DEVICE; + + usbi_err(HANDLE_CTX(handle), "streams-ioctl failed, errno=%d", errno); + return LIBUSB_ERROR_OTHER; + } + return r; +} + +static int op_alloc_streams(struct libusb_device_handle *handle, + uint32_t num_streams, unsigned char *endpoints, int num_endpoints) +{ + return do_streams_ioctl(handle, IOCTL_USBFS_ALLOC_STREAMS, + num_streams, endpoints, num_endpoints); +} + +static int op_free_streams(struct libusb_device_handle *handle, + unsigned char *endpoints, int num_endpoints) +{ + return do_streams_ioctl(handle, IOCTL_USBFS_FREE_STREAMS, 0, + endpoints, num_endpoints); +} + +static void *op_dev_mem_alloc(struct libusb_device_handle *handle, size_t len) +{ + struct linux_device_handle_priv *hpriv = usbi_get_device_handle_priv(handle); + void *buffer; + + buffer = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, hpriv->fd, 0); + if (buffer == MAP_FAILED) { + usbi_err(HANDLE_CTX(handle), "alloc dev mem failed, errno=%d", errno); + return NULL; + } + return buffer; +} + +static int op_dev_mem_free(struct libusb_device_handle *handle, void *buffer, + size_t len) +{ + if (munmap(buffer, len) != 0) { + usbi_err(HANDLE_CTX(handle), "free dev mem failed, errno=%d", errno); + return LIBUSB_ERROR_OTHER; + } else { + return LIBUSB_SUCCESS; + } +} + +static int op_kernel_driver_active(struct libusb_device_handle *handle, + uint8_t interface) +{ + struct linux_device_handle_priv *hpriv = usbi_get_device_handle_priv(handle); + int fd = hpriv->fd; + struct usbfs_getdriver getdrv; + int r; + + getdrv.interface = interface; + r = ioctl(fd, IOCTL_USBFS_GETDRIVER, &getdrv); + if (r < 0) { + if (errno == ENODATA) + return 0; + else if (errno == ENODEV) + return LIBUSB_ERROR_NO_DEVICE; + + usbi_err(HANDLE_CTX(handle), "get driver failed, errno=%d", errno); + return LIBUSB_ERROR_OTHER; + } + + return strcmp(getdrv.driver, "usbfs") != 0; +} + +static int op_detach_kernel_driver(struct libusb_device_handle *handle, + uint8_t interface) +{ + struct linux_device_handle_priv *hpriv = usbi_get_device_handle_priv(handle); + int fd = hpriv->fd; + struct usbfs_ioctl command; + struct usbfs_getdriver getdrv; + int r; + + command.ifno = interface; + command.ioctl_code = IOCTL_USBFS_DISCONNECT; + command.data = NULL; + + getdrv.interface = interface; + r = ioctl(fd, IOCTL_USBFS_GETDRIVER, &getdrv); + if (r == 0 && !strcmp(getdrv.driver, "usbfs")) + return LIBUSB_ERROR_NOT_FOUND; + + r = ioctl(fd, IOCTL_USBFS_IOCTL, &command); + if (r < 0) { + if (errno == ENODATA) + return LIBUSB_ERROR_NOT_FOUND; + else if (errno == EINVAL) + return LIBUSB_ERROR_INVALID_PARAM; + else if (errno == ENODEV) + return LIBUSB_ERROR_NO_DEVICE; + + usbi_err(HANDLE_CTX(handle), "detach failed, errno=%d", errno); + return LIBUSB_ERROR_OTHER; + } + + return 0; +} + +static int op_attach_kernel_driver(struct libusb_device_handle *handle, + uint8_t interface) +{ + struct linux_device_handle_priv *hpriv = usbi_get_device_handle_priv(handle); + int fd = hpriv->fd; + struct usbfs_ioctl command; + int r; + + command.ifno = interface; + command.ioctl_code = IOCTL_USBFS_CONNECT; + command.data = NULL; + + r = ioctl(fd, IOCTL_USBFS_IOCTL, &command); + if (r < 0) { + if (errno == ENODATA) + return LIBUSB_ERROR_NOT_FOUND; + else if (errno == EINVAL) + return LIBUSB_ERROR_INVALID_PARAM; + else if (errno == ENODEV) + return LIBUSB_ERROR_NO_DEVICE; + else if (errno == EBUSY) + return LIBUSB_ERROR_BUSY; + + usbi_err(HANDLE_CTX(handle), "attach failed, errno=%d", errno); + return LIBUSB_ERROR_OTHER; + } else if (r == 0) { + return LIBUSB_ERROR_NOT_FOUND; + } + + return 0; +} + +static int detach_kernel_driver_and_claim(struct libusb_device_handle *handle, + uint8_t interface) +{ + struct linux_device_handle_priv *hpriv = usbi_get_device_handle_priv(handle); + struct usbfs_disconnect_claim dc; + int r, fd = hpriv->fd; + + dc.interface = interface; + strcpy(dc.driver, "usbfs"); + dc.flags = USBFS_DISCONNECT_CLAIM_EXCEPT_DRIVER; + r = ioctl(fd, IOCTL_USBFS_DISCONNECT_CLAIM, &dc); + if (r == 0) + return 0; + switch (errno) { + case ENOTTY: + break; + case EBUSY: + return LIBUSB_ERROR_BUSY; + case EINVAL: + return LIBUSB_ERROR_INVALID_PARAM; + case ENODEV: + return LIBUSB_ERROR_NO_DEVICE; + default: + usbi_err(HANDLE_CTX(handle), "disconnect-and-claim failed, errno=%d", errno); + return LIBUSB_ERROR_OTHER; + } + + /* Fallback code for kernels which don't support the + disconnect-and-claim ioctl */ + r = op_detach_kernel_driver(handle, interface); + if (r != 0 && r != LIBUSB_ERROR_NOT_FOUND) + return r; + + return claim_interface(handle, interface); +} + +static int op_claim_interface(struct libusb_device_handle *handle, uint8_t interface) +{ + if (handle->auto_detach_kernel_driver) + return detach_kernel_driver_and_claim(handle, interface); + else + return claim_interface(handle, interface); +} + +static int op_release_interface(struct libusb_device_handle *handle, uint8_t interface) +{ + int r; + + r = release_interface(handle, interface); + if (r) + return r; + + if (handle->auto_detach_kernel_driver) + op_attach_kernel_driver(handle, interface); + + return 0; +} + +static void op_destroy_device(struct libusb_device *dev) +{ + struct linux_device_priv *priv = usbi_get_device_priv(dev); + + free(priv->config_descriptors); + free(priv->descriptors); + free(priv->sysfs_dir); +} + +/* URBs are discarded in reverse order of submission to avoid races. */ +static int discard_urbs(struct usbi_transfer *itransfer, int first, int last_plus_one) +{ + struct libusb_transfer *transfer = + USBI_TRANSFER_TO_LIBUSB_TRANSFER(itransfer); + struct linux_transfer_priv *tpriv = usbi_get_transfer_priv(itransfer); + struct linux_device_handle_priv *hpriv = + usbi_get_device_handle_priv(transfer->dev_handle); + int i, ret = 0; + struct usbfs_urb *urb; + + for (i = last_plus_one - 1; i >= first; i--) { + if (transfer->type == LIBUSB_TRANSFER_TYPE_ISOCHRONOUS) + urb = tpriv->iso_urbs[i]; + else + urb = &tpriv->urbs[i]; + + if (ioctl(hpriv->fd, IOCTL_USBFS_DISCARDURB, urb) == 0) + continue; + + if (errno == EINVAL) { + usbi_dbg(TRANSFER_CTX(transfer), "URB not found --> assuming ready to be reaped"); + if (i == (last_plus_one - 1)) + ret = LIBUSB_ERROR_NOT_FOUND; + } else if (errno == ENODEV) { + usbi_dbg(TRANSFER_CTX(transfer), "Device not found for URB --> assuming ready to be reaped"); + ret = LIBUSB_ERROR_NO_DEVICE; + } else { + usbi_warn(TRANSFER_CTX(transfer), "unrecognised discard errno %d", errno); + ret = LIBUSB_ERROR_OTHER; + } + } + return ret; +} + +static void free_iso_urbs(struct linux_transfer_priv *tpriv) +{ + int i; + + for (i = 0; i < tpriv->num_urbs; i++) { + struct usbfs_urb *urb = tpriv->iso_urbs[i]; + + if (!urb) + break; + free(urb); + } + + free(tpriv->iso_urbs); + tpriv->iso_urbs = NULL; +} + +static int submit_bulk_transfer(struct usbi_transfer *itransfer) +{ + struct libusb_transfer *transfer = + USBI_TRANSFER_TO_LIBUSB_TRANSFER(itransfer); + struct linux_transfer_priv *tpriv = usbi_get_transfer_priv(itransfer); + struct linux_device_handle_priv *hpriv = + usbi_get_device_handle_priv(transfer->dev_handle); + struct usbfs_urb *urbs; + int is_out = IS_XFEROUT(transfer); + int bulk_buffer_len, use_bulk_continuation; + int num_urbs; + int last_urb_partial = 0; + int r; + int i; + + /* + * Older versions of usbfs place a 16kb limit on bulk URBs. We work + * around this by splitting large transfers into 16k blocks, and then + * submit all urbs at once. it would be simpler to submit one urb at + * a time, but there is a big performance gain doing it this way. + * + * Newer versions lift the 16k limit (USBFS_CAP_NO_PACKET_SIZE_LIM), + * using arbitrary large transfers can still be a bad idea though, as + * the kernel needs to allocate physical contiguous memory for this, + * which may fail for large buffers. + * + * The kernel solves this problem by splitting the transfer into + * blocks itself when the host-controller is scatter-gather capable + * (USBFS_CAP_BULK_SCATTER_GATHER), which most controllers are. + * + * Last, there is the issue of short-transfers when splitting, for + * short split-transfers to work reliable USBFS_CAP_BULK_CONTINUATION + * is needed, but this is not always available. + */ + if (hpriv->caps & USBFS_CAP_BULK_SCATTER_GATHER) { + /* Good! Just submit everything in one go */ + bulk_buffer_len = transfer->length ? transfer->length : 1; + use_bulk_continuation = 0; + } else if (hpriv->caps & USBFS_CAP_BULK_CONTINUATION) { + /* Split the transfers and use bulk-continuation to + avoid issues with short-transfers */ + bulk_buffer_len = MAX_BULK_BUFFER_LENGTH; + use_bulk_continuation = 1; + } else if (hpriv->caps & USBFS_CAP_NO_PACKET_SIZE_LIM) { + /* Don't split, assume the kernel can alloc the buffer + (otherwise the submit will fail with -ENOMEM) */ + bulk_buffer_len = transfer->length ? transfer->length : 1; + use_bulk_continuation = 0; + } else { + /* Bad, splitting without bulk-continuation, short transfers + which end before the last urb will not work reliable! */ + /* Note we don't warn here as this is "normal" on kernels < + 2.6.32 and not a problem for most applications */ + bulk_buffer_len = MAX_BULK_BUFFER_LENGTH; + use_bulk_continuation = 0; + } + + num_urbs = transfer->length / bulk_buffer_len; + + if (transfer->length == 0) { + num_urbs = 1; + } else if ((transfer->length % bulk_buffer_len) > 0) { + last_urb_partial = 1; + num_urbs++; + } + usbi_dbg(TRANSFER_CTX(transfer), "need %d urbs for new transfer with length %d", num_urbs, transfer->length); + urbs = calloc(num_urbs, sizeof(*urbs)); + if (!urbs) + return LIBUSB_ERROR_NO_MEM; + tpriv->urbs = urbs; + tpriv->num_urbs = num_urbs; + tpriv->num_retired = 0; + tpriv->reap_action = NORMAL; + tpriv->reap_status = LIBUSB_TRANSFER_COMPLETED; + + for (i = 0; i < num_urbs; i++) { + struct usbfs_urb *urb = &urbs[i]; + + urb->usercontext = itransfer; + switch (transfer->type) { + case LIBUSB_TRANSFER_TYPE_BULK: + urb->type = USBFS_URB_TYPE_BULK; + urb->stream_id = 0; + break; + case LIBUSB_TRANSFER_TYPE_BULK_STREAM: + urb->type = USBFS_URB_TYPE_BULK; + urb->stream_id = itransfer->stream_id; + break; + case LIBUSB_TRANSFER_TYPE_INTERRUPT: + urb->type = USBFS_URB_TYPE_INTERRUPT; + break; + } + urb->endpoint = transfer->endpoint; + urb->buffer = transfer->buffer + (i * bulk_buffer_len); + + /* don't set the short not ok flag for the last URB */ + if (use_bulk_continuation && !is_out && (i < num_urbs - 1)) + urb->flags = USBFS_URB_SHORT_NOT_OK; + + if (i == num_urbs - 1 && last_urb_partial) + urb->buffer_length = transfer->length % bulk_buffer_len; + else if (transfer->length == 0) + urb->buffer_length = 0; + else + urb->buffer_length = bulk_buffer_len; + + if (i > 0 && use_bulk_continuation) + urb->flags |= USBFS_URB_BULK_CONTINUATION; + + /* we have already checked that the flag is supported */ + if (is_out && i == num_urbs - 1 && + (transfer->flags & LIBUSB_TRANSFER_ADD_ZERO_PACKET)) + urb->flags |= USBFS_URB_ZERO_PACKET; + + r = ioctl(hpriv->fd, IOCTL_USBFS_SUBMITURB, urb); + if (r == 0) + continue; + + if (errno == ENODEV) { + r = LIBUSB_ERROR_NO_DEVICE; + } else if (errno == ENOMEM) { + r = LIBUSB_ERROR_NO_MEM; + } else { + usbi_err(TRANSFER_CTX(transfer), "submiturb failed, errno=%d", errno); + r = LIBUSB_ERROR_IO; + } + + /* if the first URB submission fails, we can simply free up and + * return failure immediately. */ + if (i == 0) { + usbi_dbg(TRANSFER_CTX(transfer), "first URB failed, easy peasy"); + free(urbs); + tpriv->urbs = NULL; + return r; + } + + /* if it's not the first URB that failed, the situation is a bit + * tricky. we may need to discard all previous URBs. there are + * complications: + * - discarding is asynchronous - discarded urbs will be reaped + * later. the user must not have freed the transfer when the + * discarded URBs are reaped, otherwise libusb will be using + * freed memory. + * - the earlier URBs may have completed successfully and we do + * not want to throw away any data. + * - this URB failing may be no error; EREMOTEIO means that + * this transfer simply didn't need all the URBs we submitted + * so, we report that the transfer was submitted successfully and + * in case of error we discard all previous URBs. later when + * the final reap completes we can report error to the user, + * or success if an earlier URB was completed successfully. + */ + tpriv->reap_action = errno == EREMOTEIO ? COMPLETED_EARLY : SUBMIT_FAILED; + + /* The URBs we haven't submitted yet we count as already + * retired. */ + tpriv->num_retired += num_urbs - i; + + /* If we completed short then don't try to discard. */ + if (tpriv->reap_action == COMPLETED_EARLY) + return 0; + + discard_urbs(itransfer, 0, i); + + usbi_dbg(TRANSFER_CTX(transfer), "reporting successful submission but waiting for %d " + "discards before reporting error", i); + return 0; + } + + return 0; +} + +static int submit_iso_transfer(struct usbi_transfer *itransfer) +{ + struct libusb_transfer *transfer = + USBI_TRANSFER_TO_LIBUSB_TRANSFER(itransfer); + struct linux_transfer_priv *tpriv = usbi_get_transfer_priv(itransfer); + struct linux_device_handle_priv *hpriv = + usbi_get_device_handle_priv(transfer->dev_handle); + struct usbfs_urb **urbs; + int num_packets = transfer->num_iso_packets; + int num_packets_remaining; + int i, j; + int num_urbs; + unsigned int packet_len; + unsigned int total_len = 0; + unsigned char *urb_buffer = transfer->buffer; + + if (num_packets < 1) + return LIBUSB_ERROR_INVALID_PARAM; + + /* usbfs places arbitrary limits on iso URBs. this limit has changed + * at least three times, but we attempt to detect this limit during + * init and check it here. if the kernel rejects the request due to + * its size, we return an error indicating such to the user. + */ + for (i = 0; i < num_packets; i++) { + packet_len = transfer->iso_packet_desc[i].length; + + if (packet_len > max_iso_packet_len) { + usbi_warn(TRANSFER_CTX(transfer), + "iso packet length of %u bytes exceeds maximum of %u bytes", + packet_len, max_iso_packet_len); + return LIBUSB_ERROR_INVALID_PARAM; + } + + total_len += packet_len; + } + + if (transfer->length < (int)total_len) + return LIBUSB_ERROR_INVALID_PARAM; + + /* usbfs limits the number of iso packets per URB */ + num_urbs = (num_packets + (MAX_ISO_PACKETS_PER_URB - 1)) / MAX_ISO_PACKETS_PER_URB; + + usbi_dbg(TRANSFER_CTX(transfer), "need %d urbs for new transfer with length %d", num_urbs, transfer->length); + + urbs = calloc(num_urbs, sizeof(*urbs)); + if (!urbs) + return LIBUSB_ERROR_NO_MEM; + + tpriv->iso_urbs = urbs; + tpriv->num_urbs = num_urbs; + tpriv->num_retired = 0; + tpriv->reap_action = NORMAL; + tpriv->iso_packet_offset = 0; + + /* allocate + initialize each URB with the correct number of packets */ + num_packets_remaining = num_packets; + for (i = 0, j = 0; i < num_urbs; i++) { + int num_packets_in_urb = MIN(num_packets_remaining, MAX_ISO_PACKETS_PER_URB); + struct usbfs_urb *urb; + size_t alloc_size; + int k; + + alloc_size = sizeof(*urb) + + (num_packets_in_urb * sizeof(struct usbfs_iso_packet_desc)); + urb = calloc(1, alloc_size); + if (!urb) { + free_iso_urbs(tpriv); + return LIBUSB_ERROR_NO_MEM; + } + urbs[i] = urb; + + /* populate packet lengths */ + for (k = 0; k < num_packets_in_urb; j++, k++) { + packet_len = transfer->iso_packet_desc[j].length; + urb->buffer_length += packet_len; + urb->iso_frame_desc[k].length = packet_len; + } + + urb->usercontext = itransfer; + urb->type = USBFS_URB_TYPE_ISO; + /* FIXME: interface for non-ASAP data? */ + urb->flags = USBFS_URB_ISO_ASAP; + urb->endpoint = transfer->endpoint; + urb->number_of_packets = num_packets_in_urb; + urb->buffer = urb_buffer; + + urb_buffer += urb->buffer_length; + num_packets_remaining -= num_packets_in_urb; + } + + /* submit URBs */ + for (i = 0; i < num_urbs; i++) { + int r = ioctl(hpriv->fd, IOCTL_USBFS_SUBMITURB, urbs[i]); + + if (r == 0) + continue; + + if (errno == ENODEV) { + r = LIBUSB_ERROR_NO_DEVICE; + } else if (errno == EINVAL) { + usbi_warn(TRANSFER_CTX(transfer), "submiturb failed, transfer too large"); + r = LIBUSB_ERROR_INVALID_PARAM; + } else if (errno == EMSGSIZE) { + usbi_warn(TRANSFER_CTX(transfer), "submiturb failed, iso packet length too large"); + r = LIBUSB_ERROR_INVALID_PARAM; + } else { + usbi_err(TRANSFER_CTX(transfer), "submiturb failed, errno=%d", errno); + r = LIBUSB_ERROR_IO; + } + + /* if the first URB submission fails, we can simply free up and + * return failure immediately. */ + if (i == 0) { + usbi_dbg(TRANSFER_CTX(transfer), "first URB failed, easy peasy"); + free_iso_urbs(tpriv); + return r; + } + + /* if it's not the first URB that failed, the situation is a bit + * tricky. we must discard all previous URBs. there are + * complications: + * - discarding is asynchronous - discarded urbs will be reaped + * later. the user must not have freed the transfer when the + * discarded URBs are reaped, otherwise libusb will be using + * freed memory. + * - the earlier URBs may have completed successfully and we do + * not want to throw away any data. + * so, in this case we discard all the previous URBs BUT we report + * that the transfer was submitted successfully. then later when + * the final discard completes we can report error to the user. + */ + tpriv->reap_action = SUBMIT_FAILED; + + /* The URBs we haven't submitted yet we count as already + * retired. */ + tpriv->num_retired = num_urbs - i; + discard_urbs(itransfer, 0, i); + + usbi_dbg(TRANSFER_CTX(transfer), "reporting successful submission but waiting for %d " + "discards before reporting error", i); + return 0; + } + + return 0; +} + +static int submit_control_transfer(struct usbi_transfer *itransfer) +{ + struct linux_transfer_priv *tpriv = usbi_get_transfer_priv(itransfer); + struct libusb_transfer *transfer = + USBI_TRANSFER_TO_LIBUSB_TRANSFER(itransfer); + struct linux_device_handle_priv *hpriv = + usbi_get_device_handle_priv(transfer->dev_handle); + struct usbfs_urb *urb; + int r; + + if (transfer->length - LIBUSB_CONTROL_SETUP_SIZE > MAX_CTRL_BUFFER_LENGTH) + return LIBUSB_ERROR_INVALID_PARAM; + + urb = calloc(1, sizeof(*urb)); + if (!urb) + return LIBUSB_ERROR_NO_MEM; + tpriv->urbs = urb; + tpriv->num_urbs = 1; + tpriv->reap_action = NORMAL; + + urb->usercontext = itransfer; + urb->type = USBFS_URB_TYPE_CONTROL; + urb->endpoint = transfer->endpoint; + urb->buffer = transfer->buffer; + urb->buffer_length = transfer->length; + + r = ioctl(hpriv->fd, IOCTL_USBFS_SUBMITURB, urb); + if (r < 0) { + free(urb); + tpriv->urbs = NULL; + if (errno == ENODEV) + return LIBUSB_ERROR_NO_DEVICE; + + usbi_err(TRANSFER_CTX(transfer), "submiturb failed, errno=%d", errno); + return LIBUSB_ERROR_IO; + } + return 0; +} + +static int op_submit_transfer(struct usbi_transfer *itransfer) +{ + struct libusb_transfer *transfer = + USBI_TRANSFER_TO_LIBUSB_TRANSFER(itransfer); + + switch (transfer->type) { + case LIBUSB_TRANSFER_TYPE_CONTROL: + return submit_control_transfer(itransfer); + case LIBUSB_TRANSFER_TYPE_BULK: + case LIBUSB_TRANSFER_TYPE_BULK_STREAM: + return submit_bulk_transfer(itransfer); + case LIBUSB_TRANSFER_TYPE_INTERRUPT: + return submit_bulk_transfer(itransfer); + case LIBUSB_TRANSFER_TYPE_ISOCHRONOUS: + return submit_iso_transfer(itransfer); + default: + usbi_err(TRANSFER_CTX(transfer), "unknown transfer type %u", transfer->type); + return LIBUSB_ERROR_INVALID_PARAM; + } +} + +static int op_cancel_transfer(struct usbi_transfer *itransfer) +{ + struct linux_transfer_priv *tpriv = usbi_get_transfer_priv(itransfer); + struct libusb_transfer *transfer = + USBI_TRANSFER_TO_LIBUSB_TRANSFER(itransfer); + int r; + + if (!tpriv->urbs) + return LIBUSB_ERROR_NOT_FOUND; + + r = discard_urbs(itransfer, 0, tpriv->num_urbs); + if (r != 0) + return r; + + switch (transfer->type) { + case LIBUSB_TRANSFER_TYPE_BULK: + case LIBUSB_TRANSFER_TYPE_BULK_STREAM: + if (tpriv->reap_action == ERROR) + break; + /* else, fall through */ + default: + tpriv->reap_action = CANCELLED; + } + + return 0; +} + +static void op_clear_transfer_priv(struct usbi_transfer *itransfer) +{ + struct libusb_transfer *transfer = + USBI_TRANSFER_TO_LIBUSB_TRANSFER(itransfer); + struct linux_transfer_priv *tpriv = usbi_get_transfer_priv(itransfer); + + switch (transfer->type) { + case LIBUSB_TRANSFER_TYPE_CONTROL: + case LIBUSB_TRANSFER_TYPE_BULK: + case LIBUSB_TRANSFER_TYPE_BULK_STREAM: + case LIBUSB_TRANSFER_TYPE_INTERRUPT: + if (tpriv->urbs) { + free(tpriv->urbs); + tpriv->urbs = NULL; + } + break; + case LIBUSB_TRANSFER_TYPE_ISOCHRONOUS: + if (tpriv->iso_urbs) { + free_iso_urbs(tpriv); + tpriv->iso_urbs = NULL; + } + break; + default: + usbi_err(TRANSFER_CTX(transfer), "unknown transfer type %u", transfer->type); + } +} + +static int handle_bulk_completion(struct usbi_transfer *itransfer, + struct usbfs_urb *urb) +{ + struct linux_transfer_priv *tpriv = usbi_get_transfer_priv(itransfer); + struct libusb_transfer *transfer = USBI_TRANSFER_TO_LIBUSB_TRANSFER(itransfer); + int urb_idx = urb - tpriv->urbs; + + usbi_mutex_lock(&itransfer->lock); + usbi_dbg(TRANSFER_CTX(transfer), "handling completion status %d of bulk urb %d/%d", urb->status, + urb_idx + 1, tpriv->num_urbs); + + tpriv->num_retired++; + + if (tpriv->reap_action != NORMAL) { + /* cancelled, submit_fail, or completed early */ + usbi_dbg(TRANSFER_CTX(transfer), "abnormal reap: urb status %d", urb->status); + + /* even though we're in the process of cancelling, it's possible that + * we may receive some data in these URBs that we don't want to lose. + * examples: + * 1. while the kernel is cancelling all the packets that make up an + * URB, a few of them might complete. so we get back a successful + * cancellation *and* some data. + * 2. we receive a short URB which marks the early completion condition, + * so we start cancelling the remaining URBs. however, we're too + * slow and another URB completes (or at least completes partially). + * (this can't happen since we always use BULK_CONTINUATION.) + * + * When this happens, our objectives are not to lose any "surplus" data, + * and also to stick it at the end of the previously-received data + * (closing any holes), so that libusb reports the total amount of + * transferred data and presents it in a contiguous chunk. + */ + if (urb->actual_length > 0) { + unsigned char *target = transfer->buffer + itransfer->transferred; + + usbi_dbg(TRANSFER_CTX(transfer), "received %d bytes of surplus data", urb->actual_length); + if (urb->buffer != target) { + usbi_dbg(TRANSFER_CTX(transfer), "moving surplus data from offset %zu to offset %zu", + (unsigned char *)urb->buffer - transfer->buffer, + target - transfer->buffer); + memmove(target, urb->buffer, urb->actual_length); + } + itransfer->transferred += urb->actual_length; + } + + if (tpriv->num_retired == tpriv->num_urbs) { + usbi_dbg(TRANSFER_CTX(transfer), "abnormal reap: last URB handled, reporting"); + if (tpriv->reap_action != COMPLETED_EARLY && + tpriv->reap_status == LIBUSB_TRANSFER_COMPLETED) + tpriv->reap_status = LIBUSB_TRANSFER_ERROR; + goto completed; + } + goto out_unlock; + } + + itransfer->transferred += urb->actual_length; + + /* Many of these errors can occur on *any* urb of a multi-urb + * transfer. When they do, we tear down the rest of the transfer. + */ + switch (urb->status) { + case 0: + break; + case -EREMOTEIO: /* short transfer */ + break; + case -ENOENT: /* cancelled */ + case -ECONNRESET: + break; + case -ENODEV: + case -ESHUTDOWN: + usbi_dbg(TRANSFER_CTX(transfer), "device removed"); + tpriv->reap_status = LIBUSB_TRANSFER_NO_DEVICE; + goto cancel_remaining; + case -EPIPE: + usbi_dbg(TRANSFER_CTX(transfer), "detected endpoint stall"); + if (tpriv->reap_status == LIBUSB_TRANSFER_COMPLETED) + tpriv->reap_status = LIBUSB_TRANSFER_STALL; + goto cancel_remaining; + case -EOVERFLOW: + /* overflow can only ever occur in the last urb */ + usbi_dbg(TRANSFER_CTX(transfer), "overflow, actual_length=%d", urb->actual_length); + if (tpriv->reap_status == LIBUSB_TRANSFER_COMPLETED) + tpriv->reap_status = LIBUSB_TRANSFER_OVERFLOW; + goto completed; + case -ETIME: + case -EPROTO: + case -EILSEQ: + case -ECOMM: + case -ENOSR: + usbi_dbg(TRANSFER_CTX(transfer), "low-level bus error %d", urb->status); + tpriv->reap_action = ERROR; + goto cancel_remaining; + default: + usbi_warn(ITRANSFER_CTX(itransfer), "unrecognised urb status %d", urb->status); + tpriv->reap_action = ERROR; + goto cancel_remaining; + } + + /* if we've reaped all urbs or we got less data than requested then we're + * done */ + if (tpriv->num_retired == tpriv->num_urbs) { + usbi_dbg(TRANSFER_CTX(transfer), "all URBs in transfer reaped --> complete!"); + goto completed; + } else if (urb->actual_length < urb->buffer_length) { + usbi_dbg(TRANSFER_CTX(transfer), "short transfer %d/%d --> complete!", + urb->actual_length, urb->buffer_length); + if (tpriv->reap_action == NORMAL) + tpriv->reap_action = COMPLETED_EARLY; + } else { + goto out_unlock; + } + +cancel_remaining: + if (tpriv->reap_action == ERROR && tpriv->reap_status == LIBUSB_TRANSFER_COMPLETED) + tpriv->reap_status = LIBUSB_TRANSFER_ERROR; + + if (tpriv->num_retired == tpriv->num_urbs) /* nothing to cancel */ + goto completed; + + /* cancel remaining urbs and wait for their completion before + * reporting results */ + discard_urbs(itransfer, urb_idx + 1, tpriv->num_urbs); + +out_unlock: + usbi_mutex_unlock(&itransfer->lock); + return 0; + +completed: + free(tpriv->urbs); + tpriv->urbs = NULL; + usbi_mutex_unlock(&itransfer->lock); + return tpriv->reap_action == CANCELLED ? + usbi_handle_transfer_cancellation(itransfer) : + usbi_handle_transfer_completion(itransfer, tpriv->reap_status); +} + +static int handle_iso_completion(struct usbi_transfer *itransfer, + struct usbfs_urb *urb) +{ + struct libusb_transfer *transfer = + USBI_TRANSFER_TO_LIBUSB_TRANSFER(itransfer); + struct linux_transfer_priv *tpriv = usbi_get_transfer_priv(itransfer); + int num_urbs = tpriv->num_urbs; + int urb_idx = 0; + int i; + enum libusb_transfer_status status = LIBUSB_TRANSFER_COMPLETED; + + usbi_mutex_lock(&itransfer->lock); + for (i = 0; i < num_urbs; i++) { + if (urb == tpriv->iso_urbs[i]) { + urb_idx = i + 1; + break; + } + } + if (urb_idx == 0) { + usbi_err(TRANSFER_CTX(transfer), "could not locate urb!"); + usbi_mutex_unlock(&itransfer->lock); + return LIBUSB_ERROR_NOT_FOUND; + } + + usbi_dbg(TRANSFER_CTX(transfer), "handling completion status %d of iso urb %d/%d", urb->status, + urb_idx, num_urbs); + + /* copy isochronous results back in */ + + for (i = 0; i < urb->number_of_packets; i++) { + struct usbfs_iso_packet_desc *urb_desc = &urb->iso_frame_desc[i]; + struct libusb_iso_packet_descriptor *lib_desc = + &transfer->iso_packet_desc[tpriv->iso_packet_offset++]; + + lib_desc->status = LIBUSB_TRANSFER_COMPLETED; + switch (urb_desc->status) { + case 0: + break; + case -ENOENT: /* cancelled */ + case -ECONNRESET: + break; + case -ENODEV: + case -ESHUTDOWN: + usbi_dbg(TRANSFER_CTX(transfer), "packet %d - device removed", i); + lib_desc->status = LIBUSB_TRANSFER_NO_DEVICE; + break; + case -EPIPE: + usbi_dbg(TRANSFER_CTX(transfer), "packet %d - detected endpoint stall", i); + lib_desc->status = LIBUSB_TRANSFER_STALL; + break; + case -EOVERFLOW: + usbi_dbg(TRANSFER_CTX(transfer), "packet %d - overflow error", i); + lib_desc->status = LIBUSB_TRANSFER_OVERFLOW; + break; + case -ETIME: + case -EPROTO: + case -EILSEQ: + case -ECOMM: + case -ENOSR: + case -EXDEV: + usbi_dbg(TRANSFER_CTX(transfer), "packet %d - low-level USB error %d", i, urb_desc->status); + lib_desc->status = LIBUSB_TRANSFER_ERROR; + break; + default: + usbi_warn(TRANSFER_CTX(transfer), "packet %d - unrecognised urb status %d", + i, urb_desc->status); + lib_desc->status = LIBUSB_TRANSFER_ERROR; + break; + } + lib_desc->actual_length = urb_desc->actual_length; + } + + tpriv->num_retired++; + + if (tpriv->reap_action != NORMAL) { /* cancelled or submit_fail */ + usbi_dbg(TRANSFER_CTX(transfer), "CANCEL: urb status %d", urb->status); + + if (tpriv->num_retired == num_urbs) { + usbi_dbg(TRANSFER_CTX(transfer), "CANCEL: last URB handled, reporting"); + free_iso_urbs(tpriv); + if (tpriv->reap_action == CANCELLED) { + usbi_mutex_unlock(&itransfer->lock); + return usbi_handle_transfer_cancellation(itransfer); + } else { + usbi_mutex_unlock(&itransfer->lock); + return usbi_handle_transfer_completion(itransfer, LIBUSB_TRANSFER_ERROR); + } + } + goto out; + } + + switch (urb->status) { + case 0: + break; + case -ENOENT: /* cancelled */ + case -ECONNRESET: + break; + case -ESHUTDOWN: + usbi_dbg(TRANSFER_CTX(transfer), "device removed"); + status = LIBUSB_TRANSFER_NO_DEVICE; + break; + default: + usbi_warn(TRANSFER_CTX(transfer), "unrecognised urb status %d", urb->status); + status = LIBUSB_TRANSFER_ERROR; + break; + } + + /* if we've reaped all urbs then we're done */ + if (tpriv->num_retired == num_urbs) { + usbi_dbg(TRANSFER_CTX(transfer), "all URBs in transfer reaped --> complete!"); + free_iso_urbs(tpriv); + usbi_mutex_unlock(&itransfer->lock); + return usbi_handle_transfer_completion(itransfer, status); + } + +out: + usbi_mutex_unlock(&itransfer->lock); + return 0; +} + +static int handle_control_completion(struct usbi_transfer *itransfer, + struct usbfs_urb *urb) +{ + struct linux_transfer_priv *tpriv = usbi_get_transfer_priv(itransfer); + int status; + + usbi_mutex_lock(&itransfer->lock); + usbi_dbg(ITRANSFER_CTX(itransfer), "handling completion status %d", urb->status); + + itransfer->transferred += urb->actual_length; + + if (tpriv->reap_action == CANCELLED) { + if (urb->status && urb->status != -ENOENT) + usbi_warn(ITRANSFER_CTX(itransfer), "cancel: unrecognised urb status %d", + urb->status); + free(tpriv->urbs); + tpriv->urbs = NULL; + usbi_mutex_unlock(&itransfer->lock); + return usbi_handle_transfer_cancellation(itransfer); + } + + switch (urb->status) { + case 0: + status = LIBUSB_TRANSFER_COMPLETED; + break; + case -ENOENT: /* cancelled */ + status = LIBUSB_TRANSFER_CANCELLED; + break; + case -ENODEV: + case -ESHUTDOWN: + usbi_dbg(ITRANSFER_CTX(itransfer), "device removed"); + status = LIBUSB_TRANSFER_NO_DEVICE; + break; + case -EPIPE: + usbi_dbg(ITRANSFER_CTX(itransfer), "unsupported control request"); + status = LIBUSB_TRANSFER_STALL; + break; + case -EOVERFLOW: + usbi_dbg(ITRANSFER_CTX(itransfer), "overflow, actual_length=%d", urb->actual_length); + status = LIBUSB_TRANSFER_OVERFLOW; + break; + case -ETIME: + case -EPROTO: + case -EILSEQ: + case -ECOMM: + case -ENOSR: + usbi_dbg(ITRANSFER_CTX(itransfer), "low-level bus error %d", urb->status); + status = LIBUSB_TRANSFER_ERROR; + break; + default: + usbi_warn(ITRANSFER_CTX(itransfer), "unrecognised urb status %d", urb->status); + status = LIBUSB_TRANSFER_ERROR; + break; + } + + free(tpriv->urbs); + tpriv->urbs = NULL; + usbi_mutex_unlock(&itransfer->lock); + return usbi_handle_transfer_completion(itransfer, status); +} + +static int reap_for_handle(struct libusb_device_handle *handle) +{ + struct linux_device_handle_priv *hpriv = usbi_get_device_handle_priv(handle); + int r; + struct usbfs_urb *urb = NULL; + struct usbi_transfer *itransfer; + struct libusb_transfer *transfer; + + r = ioctl(hpriv->fd, IOCTL_USBFS_REAPURBNDELAY, &urb); + if (r < 0) { + if (errno == EAGAIN) + return 1; + if (errno == ENODEV) + return LIBUSB_ERROR_NO_DEVICE; + + usbi_err(HANDLE_CTX(handle), "reap failed, errno=%d", errno); + return LIBUSB_ERROR_IO; + } + + itransfer = urb->usercontext; + transfer = USBI_TRANSFER_TO_LIBUSB_TRANSFER(itransfer); + + usbi_dbg(HANDLE_CTX(handle), "urb type=%u status=%d transferred=%d", urb->type, urb->status, urb->actual_length); + + switch (transfer->type) { + case LIBUSB_TRANSFER_TYPE_ISOCHRONOUS: + return handle_iso_completion(itransfer, urb); + case LIBUSB_TRANSFER_TYPE_BULK: + case LIBUSB_TRANSFER_TYPE_BULK_STREAM: + case LIBUSB_TRANSFER_TYPE_INTERRUPT: + return handle_bulk_completion(itransfer, urb); + case LIBUSB_TRANSFER_TYPE_CONTROL: + return handle_control_completion(itransfer, urb); + default: + usbi_err(HANDLE_CTX(handle), "unrecognised transfer type %u", transfer->type); + return LIBUSB_ERROR_OTHER; + } +} + +static int op_handle_events(struct libusb_context *ctx, + void *event_data, unsigned int count, unsigned int num_ready) +{ + struct pollfd *fds = event_data; + unsigned int n; + int r; + + usbi_mutex_lock(&ctx->open_devs_lock); + for (n = 0; n < count && num_ready > 0; n++) { + struct pollfd *pollfd = &fds[n]; + struct libusb_device_handle *handle; + struct linux_device_handle_priv *hpriv = NULL; + int reap_count; + + if (!pollfd->revents) + continue; + + num_ready--; + for_each_open_device(ctx, handle) { + hpriv = usbi_get_device_handle_priv(handle); + if (hpriv->fd == pollfd->fd) + break; + } + + if (!hpriv || hpriv->fd != pollfd->fd) { + usbi_err(ctx, "cannot find handle for fd %d", + pollfd->fd); + continue; + } + + if (pollfd->revents & POLLERR) { + /* remove the fd from the pollfd set so that it doesn't continuously + * trigger an event, and flag that it has been removed so op_close() + * doesn't try to remove it a second time */ + usbi_remove_event_source(HANDLE_CTX(handle), hpriv->fd); + hpriv->fd_removed = 1; + + /* device will still be marked as attached if hotplug monitor thread + * hasn't processed remove event yet */ + usbi_mutex_static_lock(&linux_hotplug_lock); + if (usbi_atomic_load(&handle->dev->attached)) + linux_device_disconnected(handle->dev->bus_number, + handle->dev->device_address); + usbi_mutex_static_unlock(&linux_hotplug_lock); + + if (hpriv->caps & USBFS_CAP_REAP_AFTER_DISCONNECT) { + do { + r = reap_for_handle(handle); + } while (r == 0); + } + + usbi_handle_disconnect(ctx, handle); + continue; + } + + reap_count = 0; + do { + r = reap_for_handle(handle); + } while (r == 0 && ++reap_count <= 25); + + if (r == 1 || r == LIBUSB_ERROR_NO_DEVICE) + continue; + else if (r < 0) + goto out; + } + + r = 0; +out: + usbi_mutex_unlock(&ctx->open_devs_lock); + return r; +} + +const struct usbi_os_backend usbi_backend = { + .name = "Linux usbfs", + .caps = USBI_CAP_HAS_HID_ACCESS|USBI_CAP_SUPPORTS_DETACH_KERNEL_DRIVER, + .init = op_init, + .exit = op_exit, + .set_option = op_set_option, + .get_device_string = op_get_device_string, + .hotplug_poll = op_hotplug_poll, + .get_active_config_descriptor = op_get_active_config_descriptor, + .get_config_descriptor = op_get_config_descriptor, + .get_config_descriptor_by_value = op_get_config_descriptor_by_value, + + .wrap_sys_device = op_wrap_sys_device, + .open = op_open, + .close = op_close, + .get_configuration = op_get_configuration, + .set_configuration = op_set_configuration, + .claim_interface = op_claim_interface, + .release_interface = op_release_interface, + + .set_interface_altsetting = op_set_interface, + .clear_halt = op_clear_halt, + .reset_device = op_reset_device, + + .alloc_streams = op_alloc_streams, + .free_streams = op_free_streams, + + .dev_mem_alloc = op_dev_mem_alloc, + .dev_mem_free = op_dev_mem_free, + + .kernel_driver_active = op_kernel_driver_active, + .detach_kernel_driver = op_detach_kernel_driver, + .attach_kernel_driver = op_attach_kernel_driver, + + .destroy_device = op_destroy_device, + + .submit_transfer = op_submit_transfer, + .cancel_transfer = op_cancel_transfer, + .clear_transfer_priv = op_clear_transfer_priv, + + .handle_events = op_handle_events, + + .context_priv_size = sizeof(struct linux_context_priv), + .device_priv_size = sizeof(struct linux_device_priv), + .device_handle_priv_size = sizeof(struct linux_device_handle_priv), + .transfer_priv_size = sizeof(struct linux_transfer_priv), +}; diff --git a/usbaudio/src/main/cpp/external/libusb/libusb/os/linux_usbfs.h b/usbaudio/src/main/cpp/external/libusb/libusb/os/linux_usbfs.h new file mode 100644 index 000000000..5c6467426 --- /dev/null +++ b/usbaudio/src/main/cpp/external/libusb/libusb/os/linux_usbfs.h @@ -0,0 +1,221 @@ +/* + * usbfs header structures + * Copyright © 2007 Daniel Drake + * Copyright © 2001 Johannes Erdfelt + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef LIBUSB_USBFS_H +#define LIBUSB_USBFS_H + +#include +#include + +#define SYSFS_MOUNT_PATH "/sys" +#define SYSFS_DEVICE_PATH SYSFS_MOUNT_PATH "/bus/usb/devices" + +struct usbfs_ctrltransfer { + /* keep in sync with usbdevice_fs.h:usbdevfs_ctrltransfer */ + __u8 bmRequestType; + __u8 bRequest; + __u16 wValue; + __u16 wIndex; + __u16 wLength; + __u32 timeout; /* in milliseconds */ + + /* pointer to data */ + void *data; +}; + +struct usbfs_setinterface { + /* keep in sync with usbdevice_fs.h:usbdevfs_setinterface */ + unsigned int interface; + unsigned int altsetting; +}; + +#define USBFS_MAXDRIVERNAME 255 + +struct usbfs_getdriver { + unsigned int interface; + char driver[USBFS_MAXDRIVERNAME + 1]; +}; + +#define USBFS_URB_SHORT_NOT_OK 0x01 +#define USBFS_URB_ISO_ASAP 0x02 +#define USBFS_URB_BULK_CONTINUATION 0x04 +#define USBFS_URB_QUEUE_BULK 0x10 +#define USBFS_URB_ZERO_PACKET 0x40 + +#define USBFS_URB_TYPE_ISO 0 +#define USBFS_URB_TYPE_INTERRUPT 1 +#define USBFS_URB_TYPE_CONTROL 2 +#define USBFS_URB_TYPE_BULK 3 + +struct usbfs_iso_packet_desc { + unsigned int length; + unsigned int actual_length; + unsigned int status; +}; + +#define MAX_BULK_BUFFER_LENGTH 16384 +#define MAX_CTRL_BUFFER_LENGTH 4096 + +#define MAX_ISO_PACKETS_PER_URB 128 + +struct usbfs_urb { + unsigned char type; + unsigned char endpoint; + int status; + unsigned int flags; + void *buffer; + int buffer_length; + int actual_length; + int start_frame; + union { + int number_of_packets; /* Only used for isoc urbs */ + unsigned int stream_id; /* Only used with bulk streams */ + }; + int error_count; + unsigned int signr; + void *usercontext; + struct usbfs_iso_packet_desc iso_frame_desc[0]; +}; + +struct usbfs_connectinfo { + unsigned int devnum; + unsigned char slow; +}; + +struct usbfs_ioctl { + int ifno; /* interface 0..N ; negative numbers reserved */ + int ioctl_code; /* MUST encode size + direction of data so the + * macros in give correct values */ + void *data; /* param buffer (in, or out) */ +}; + +#define USBFS_CAP_ZERO_PACKET 0x01 +#define USBFS_CAP_BULK_CONTINUATION 0x02 +#define USBFS_CAP_NO_PACKET_SIZE_LIM 0x04 +#define USBFS_CAP_BULK_SCATTER_GATHER 0x08 +#define USBFS_CAP_REAP_AFTER_DISCONNECT 0x10 + +#define USBFS_DISCONNECT_CLAIM_IF_DRIVER 0x01 +#define USBFS_DISCONNECT_CLAIM_EXCEPT_DRIVER 0x02 + +struct usbfs_disconnect_claim { + unsigned int interface; + unsigned int flags; + char driver[USBFS_MAXDRIVERNAME + 1]; +}; + +struct usbfs_streams { + unsigned int num_streams; /* Not used by USBDEVFS_FREE_STREAMS */ + unsigned int num_eps; + unsigned char eps[0]; +}; + +#define USBFS_SPEED_UNKNOWN 0 +#define USBFS_SPEED_LOW 1 +#define USBFS_SPEED_FULL 2 +#define USBFS_SPEED_HIGH 3 +#define USBFS_SPEED_WIRELESS 4 +#define USBFS_SPEED_SUPER 5 +#define USBFS_SPEED_SUPER_PLUS 6 + +#define IOCTL_USBFS_CONTROL _IOWR('U', 0, struct usbfs_ctrltransfer) +#define IOCTL_USBFS_SETINTERFACE _IOR('U', 4, struct usbfs_setinterface) +#define IOCTL_USBFS_SETCONFIGURATION _IOR('U', 5, unsigned int) +#define IOCTL_USBFS_GETDRIVER _IOW('U', 8, struct usbfs_getdriver) +#define IOCTL_USBFS_SUBMITURB _IOR('U', 10, struct usbfs_urb) +#define IOCTL_USBFS_DISCARDURB _IO('U', 11) +#define IOCTL_USBFS_REAPURBNDELAY _IOW('U', 13, void *) +#define IOCTL_USBFS_CLAIMINTERFACE _IOR('U', 15, unsigned int) +#define IOCTL_USBFS_RELEASEINTERFACE _IOR('U', 16, unsigned int) +#define IOCTL_USBFS_CONNECTINFO _IOW('U', 17, struct usbfs_connectinfo) +#define IOCTL_USBFS_IOCTL _IOWR('U', 18, struct usbfs_ioctl) +#define IOCTL_USBFS_RESET _IO('U', 20) +#define IOCTL_USBFS_CLEAR_HALT _IOR('U', 21, unsigned int) +#define IOCTL_USBFS_DISCONNECT _IO('U', 22) +#define IOCTL_USBFS_CONNECT _IO('U', 23) +#define IOCTL_USBFS_GET_CAPABILITIES _IOR('U', 26, __u32) +#define IOCTL_USBFS_DISCONNECT_CLAIM _IOR('U', 27, struct usbfs_disconnect_claim) +#define IOCTL_USBFS_ALLOC_STREAMS _IOR('U', 28, struct usbfs_streams) +#define IOCTL_USBFS_FREE_STREAMS _IOR('U', 29, struct usbfs_streams) +#define IOCTL_USBFS_DROP_PRIVILEGES _IOW('U', 30, __u32) +#define IOCTL_USBFS_GET_SPEED _IO('U', 31) + +extern usbi_mutex_static_t linux_hotplug_lock; + +#ifdef HAVE_LIBUDEV +int linux_udev_start_event_monitor(void); +int linux_udev_stop_event_monitor(void); +int linux_udev_scan_devices(struct libusb_context *ctx); +void linux_udev_hotplug_poll(void); +#else +int linux_netlink_start_event_monitor(void); +int linux_netlink_stop_event_monitor(void); +void linux_netlink_hotplug_poll(void); +#endif + +static inline int linux_start_event_monitor(void) +{ +#if defined(HAVE_LIBUDEV) + return linux_udev_start_event_monitor(); +/* +* __ANDROID__: preprocessor macro defined automatically by GCC for all Android +* targets (i.e. both Android native applications, and Android OS-level +* services) +* +* ANDROID_OS: compilation flag that should be set for using libusb from programs +* running on Android at OS level (e.g. Android platform services). +* The programs using libusb built with the ANDROID_OS flag must have +* permission to access netlink sockets. +*/ +#elif !defined(__ANDROID__) || defined(ANDROID_OS) + return linux_netlink_start_event_monitor(); +#else + return LIBUSB_SUCCESS; +#endif +} + +static inline void linux_stop_event_monitor(void) +{ +#if defined(HAVE_LIBUDEV) + linux_udev_stop_event_monitor(); +#elif !defined(__ANDROID__) || defined(ANDROID_OS) + linux_netlink_stop_event_monitor(); +#endif +} + +static inline void linux_hotplug_poll(void) +{ +#if defined(HAVE_LIBUDEV) + linux_udev_hotplug_poll(); +#elif !defined(__ANDROID__) || defined(ANDROID_OS) + linux_netlink_hotplug_poll(); +#endif +} + +void linux_hotplug_enumerate(uint8_t busnum, uint8_t devaddr, const char *sys_name); +void linux_device_disconnected(uint8_t busnum, uint8_t devaddr); + +int linux_get_device_address(struct libusb_context *ctx, int detached, + uint8_t *busnum, uint8_t *devaddr, const char *dev_node, + const char *sys_name, int fd); +int linux_enumerate_device(struct libusb_context *ctx, + uint8_t busnum, uint8_t devaddr, const char *sysfs_dir); + +#endif diff --git a/usbaudio/src/main/cpp/external/libusb/libusb/os/threads_posix.c b/usbaudio/src/main/cpp/external/libusb/libusb/os/threads_posix.c new file mode 100644 index 000000000..786a33499 --- /dev/null +++ b/usbaudio/src/main/cpp/external/libusb/libusb/os/threads_posix.c @@ -0,0 +1,126 @@ +/* + * libusb synchronization using POSIX Threads + * + * Copyright © 2011 Vitali Lovich + * Copyright © 2011 Peter Stuge + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "libusbi.h" + +#include +#include +#if defined(__ANDROID__) +# include +#elif defined(__HAIKU__) +# include +#elif defined(__linux__) +# include +# include +#elif defined(__NetBSD__) +# include +#elif defined(__OpenBSD__) +# include +#elif defined(__sun__) +# include +#endif + +void usbi_cond_init(usbi_cond_t *cond) +{ +#ifdef HAVE_PTHREAD_CONDATTR_SETCLOCK + pthread_condattr_t condattr; + + PTHREAD_CHECK(pthread_condattr_init(&condattr)); + PTHREAD_CHECK(pthread_condattr_setclock(&condattr, CLOCK_MONOTONIC)); + PTHREAD_CHECK(pthread_cond_init(cond, &condattr)); + PTHREAD_CHECK(pthread_condattr_destroy(&condattr)); +#else + PTHREAD_CHECK(pthread_cond_init(cond, NULL)); +#endif +} + +int usbi_cond_timedwait(usbi_cond_t *cond, + usbi_mutex_t *mutex, const struct timeval *tv) +{ + struct timespec timeout; + int r; + +#ifdef HAVE_PTHREAD_CONDATTR_SETCLOCK + usbi_get_monotonic_time(&timeout); +#else + usbi_get_real_time(&timeout); +#endif + + timeout.tv_sec += tv->tv_sec; + timeout.tv_nsec += tv->tv_usec * 1000L; + if (timeout.tv_nsec >= NSEC_PER_SEC) { + timeout.tv_nsec -= NSEC_PER_SEC; + timeout.tv_sec++; + } + + r = pthread_cond_timedwait(cond, mutex, &timeout); + if (r == 0) + return 0; + else if (r == ETIMEDOUT) + return LIBUSB_ERROR_TIMEOUT; + else + return LIBUSB_ERROR_OTHER; +} + +unsigned long usbi_get_tid(void) +{ + static _Thread_local unsigned long tl_tid; + unsigned long tid; + + if (tl_tid) + return tl_tid; + +#if defined(__ANDROID__) + tid = (unsigned long)gettid(); +#elif defined(__APPLE__) +#ifdef HAVE_PTHREAD_THREADID_NP + uint64_t thread_id; + + if (pthread_threadid_np(NULL, &thread_id) == 0) + tid = (unsigned long)thread_id; + else + tid = ULONG_MAX; +#else + tid = (unsigned long)pthread_mach_thread_np(pthread_self()); +#endif +#elif defined(__HAIKU__) + tid = (unsigned long)get_pthread_thread_id(pthread_self()); +#elif defined(__linux__) + tid = (unsigned long)syscall(SYS_gettid); +#elif defined(__NetBSD__) + tid = (unsigned long)_lwp_self(); +#elif defined(__OpenBSD__) + tid = (unsigned long)getthrid(); +#elif defined(__sun__) + tid = (unsigned long)_lwp_self(); +#else + tid = ULONG_MAX; +#endif + + if (tid == ULONG_MAX) { + /* If we don't have a thread ID, at least return a unique + * value that can be used to distinguish individual + * threads. */ + tid = (unsigned long)(uintptr_t)pthread_self(); + } + + return tl_tid = tid; +} diff --git a/usbaudio/src/main/cpp/external/libusb/libusb/os/threads_posix.h b/usbaudio/src/main/cpp/external/libusb/libusb/os/threads_posix.h new file mode 100644 index 000000000..eb7e24686 --- /dev/null +++ b/usbaudio/src/main/cpp/external/libusb/libusb/os/threads_posix.h @@ -0,0 +1,104 @@ +/* + * libusb synchronization using POSIX Threads + * + * Copyright © 2010 Peter Stuge + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef LIBUSB_THREADS_POSIX_H +#define LIBUSB_THREADS_POSIX_H + +#include + +#define PTHREAD_CHECK(expression) ASSERT_EQ(expression, 0) + +#define USBI_MUTEX_INITIALIZER PTHREAD_MUTEX_INITIALIZER +typedef pthread_mutex_t usbi_mutex_static_t; +static inline void usbi_mutex_static_lock(usbi_mutex_static_t *mutex) +{ + PTHREAD_CHECK(pthread_mutex_lock(mutex)); +} +static inline void usbi_mutex_static_unlock(usbi_mutex_static_t *mutex) +{ + PTHREAD_CHECK(pthread_mutex_unlock(mutex)); +} + +typedef pthread_mutex_t usbi_mutex_t; +static inline void usbi_mutex_init(usbi_mutex_t *mutex) +{ + PTHREAD_CHECK(pthread_mutex_init(mutex, NULL)); +} +static inline void usbi_mutex_lock(usbi_mutex_t *mutex) +{ + PTHREAD_CHECK(pthread_mutex_lock(mutex)); +} +static inline void usbi_mutex_unlock(usbi_mutex_t *mutex) +{ + PTHREAD_CHECK(pthread_mutex_unlock(mutex)); +} +static inline int usbi_mutex_trylock(usbi_mutex_t *mutex) +{ + int mutexIsLocked = pthread_mutex_trylock(mutex) == 0; + return mutexIsLocked; +} +static inline void usbi_mutex_destroy(usbi_mutex_t *mutex) +{ + PTHREAD_CHECK(pthread_mutex_destroy(mutex)); +} + +#define USBI_COND_INITIALIZER PTHREAD_COND_INITIALIZER +typedef pthread_cond_t usbi_cond_t; +void usbi_cond_init(usbi_cond_t *cond); +static inline void usbi_cond_wait(usbi_cond_t *cond, usbi_mutex_t *mutex) +{ + PTHREAD_CHECK(pthread_cond_wait(cond, mutex)); +} +int usbi_cond_timedwait(usbi_cond_t *cond, + usbi_mutex_t *mutex, const struct timeval *tv); +static inline void usbi_cond_signal(usbi_cond_t *cond) +{ + PTHREAD_CHECK(pthread_cond_signal(cond)); +} +static inline void usbi_cond_broadcast(usbi_cond_t *cond) +{ + PTHREAD_CHECK(pthread_cond_broadcast(cond)); +} +static inline void usbi_cond_destroy(usbi_cond_t *cond) +{ + PTHREAD_CHECK(pthread_cond_destroy(cond)); +} + +typedef pthread_key_t usbi_tls_key_t; +static inline void usbi_tls_key_create(usbi_tls_key_t *key) +{ + PTHREAD_CHECK(pthread_key_create(key, NULL)); +} +static inline void *usbi_tls_key_get(usbi_tls_key_t key) +{ + return pthread_getspecific(key); +} +static inline void usbi_tls_key_set(usbi_tls_key_t key, void *ptr) +{ + PTHREAD_CHECK(pthread_setspecific(key, ptr)); +} +static inline void usbi_tls_key_delete(usbi_tls_key_t key) +{ + PTHREAD_CHECK(pthread_key_delete(key)); +} + +unsigned long usbi_get_tid(void); + +#endif /* LIBUSB_THREADS_POSIX_H */ diff --git a/usbaudio/src/main/cpp/external/libusb/libusb/strerror.c b/usbaudio/src/main/cpp/external/libusb/libusb/strerror.c new file mode 100644 index 000000000..eb355034b --- /dev/null +++ b/usbaudio/src/main/cpp/external/libusb/libusb/strerror.c @@ -0,0 +1,223 @@ +/* + * libusb strerror code + * Copyright © 2013 Hans de Goede + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "libusbi.h" + +#include +#include + +/** \ingroup libusb_misc + * How to add a new \ref libusb_strerror() translation: + *
    + *
  1. Download the latest \c strerror.c from:
    + * https://raw.github.com/libusb/libusb/master/libusb/strerror.c
  2. + *
  3. Open the file in an UTF-8 capable editor
  4. + *
  5. Add the 2 letter ISO 639-1 + * code for your locale at the end of \c usbi_locale_supported[]
    + * Eg. for Chinese, you would add "zh" so that: + * \code... usbi_locale_supported[] = { "en", "nl", "fr" };\endcode + * becomes: + * \code... usbi_locale_supported[] = { "en", "nl", "fr", "zh" };\endcode
  6. + *
  7. Copy the { / * English (en) * / ... } section and add it at the end of \c usbi_localized_errors
    + * Eg. for Chinese, the last section of \c usbi_localized_errors could look like: + * \code + * }, { / * Chinese (zh) * / + * "Success", + * ... + * "Other error", + * }, + * };\endcode
  8. + *
  9. Translate each of the English messages from the section you copied into your language
  10. + *
  11. Save the file (in UTF-8 format) and send it to \c libusb-devel\@lists.sourceforge.net
  12. + *
+ */ + +static const char * const usbi_locale_supported[] = { "en", "nl", "fr", "ru", "de", "hu" }; +static const char * const usbi_localized_errors[ARRAYSIZE(usbi_locale_supported)][LIBUSB_ERROR_COUNT] = { + { /* English (en) */ + "Success", + "Input/Output Error", + "Invalid parameter", + "Access denied (insufficient permissions)", + "No such device (it may have been disconnected)", + "Entity not found", + "Resource busy", + "Operation timed out", + "Overflow", + "Pipe error", + "System call interrupted (perhaps due to signal)", + "Insufficient memory", + "Operation not supported or unimplemented on this platform", + "Other error", + }, { /* Dutch (nl) */ + "Gelukt", + "Invoer-/uitvoerfout", + "Ongeldig argument", + "Toegang geweigerd (onvoldoende toegangsrechten)", + "Apparaat bestaat niet (verbinding met apparaat verbroken?)", + "Niet gevonden", + "Apparaat of hulpbron is bezig", + "Bewerking verlopen", + "Waarde is te groot", + "Gebroken pijp", + "Onderbroken systeemaanroep", + "Onvoldoende geheugen beschikbaar", + "Bewerking wordt niet ondersteund", + "Andere fout", + }, { /* French (fr) */ + "Succès", + "Erreur d'entrée/sortie", + "Paramètre invalide", + "Accès refusé (permissions insuffisantes)", + "Périphérique introuvable (peut-être déconnecté)", + "Elément introuvable", + "Resource déjà occupée", + "Operation expirée", + "Débordement", + "Erreur de pipe", + "Appel système abandonné (peut-être à cause d’un signal)", + "Mémoire insuffisante", + "Opération non supportée or non implémentée sur cette plateforme", + "Autre erreur", + }, { /* Russian (ru) */ + "Успех", + "Ошибка ввода/вывода", + "Неверный параметр", + "Доступ запрещён (не хватает прав)", + "Устройство отсутствует (возможно, оно было отсоединено)", + "Элемент не найден", + "Ресурс занят", + "Истекло время ожидания операции", + "Переполнение", + "Ошибка канала", + "Системный вызов прерван (возможно, сигналом)", + "Память исчерпана", + "Операция не поддерживается данной платформой", + "Неизвестная ошибка" + }, { /* German (de) */ + "Erfolgreich", + "Eingabe-/Ausgabefehler", + "Ungültiger Parameter", + "Keine Berechtigung (Zugriffsrechte fehlen)", + "Kein passendes Gerät gefunden (es könnte entfernt worden sein)", + "Entität nicht gefunden", + "Die Ressource ist belegt", + "Die Wartezeit für die Operation ist abgelaufen", + "Mehr Daten empfangen als erwartet", + "Datenübergabe unterbrochen (broken pipe)", + "Unterbrechung während des Betriebssystemaufrufs", + "Nicht genügend Hauptspeicher verfügbar", + "Die Operation wird nicht unterstützt oder ist auf dieser Platform nicht implementiert", + "Allgemeiner Fehler", + }, { /* Hungarian (hu) */ + "Sikeres", + "Be-/kimeneti hiba", + "Érvénytelen paraméter", + "Hozzáférés megtagadva", + "Az eszköz nem található (eltávolították?)", + "Nem található", + "Az erőforrás foglalt", + "Időtúllépés", + "Túlcsordulás", + "Törött adatcsatorna", + "Rendszerhívás megszakítva", + "Nincs elég memória", + "A művelet nem támogatott ezen a rendszeren", + "Általános hiba", + }, +}; + +static const char * const (*usbi_error_strings)[LIBUSB_ERROR_COUNT] = &usbi_localized_errors[0]; + +/** \ingroup libusb_misc + * Set the language, and only the language, not the encoding! used for + * translatable libusb messages. + * + * This takes a locale string in the default setlocale format: lang[-region] + * or lang[_country_region][.codeset]. Only the lang part of the string is + * used, and only 2 letter ISO 639-1 codes are accepted for it, such as "de". + * The optional region, country_region or codeset parts are ignored. This + * means that functions which return translatable strings will NOT honor the + * specified encoding. + * All strings returned are encoded as UTF-8 strings. + * + * If libusb_setlocale() is not called, all messages will be in English. + * + * The following functions return translatable strings: libusb_strerror(). + * Note that the libusb log messages controlled through LIBUSB_OPTION_LOG_LEVEL + * are not translated, they are always in English. + * + * For POSIX UTF-8 environments if you want libusb to follow the standard + * locale settings, call libusb_setlocale(setlocale(LC_MESSAGES, NULL)), + * after your app has done its locale setup. + * + * \param locale locale-string in the form of lang[_country_region][.codeset] + * or lang[-region], where lang is a 2 letter ISO 639-1 code + * \returns \ref LIBUSB_SUCCESS on success + * \returns \ref LIBUSB_ERROR_INVALID_PARAM if the locale doesn't meet the requirements + * \returns \ref LIBUSB_ERROR_NOT_FOUND if the requested language is not supported + * \returns a LIBUSB_ERROR code on other errors + */ + +int API_EXPORTED libusb_setlocale(const char *locale) +{ + size_t i; + + if (!locale || strlen(locale) < 2 + || (locale[2] != '\0' && locale[2] != '-' && locale[2] != '_' && locale[2] != '.')) + return LIBUSB_ERROR_INVALID_PARAM; + + for (i = 0; i < ARRAYSIZE(usbi_locale_supported); i++) { + if (usbi_locale_supported[i][0] == tolower((unsigned char)locale[0]) + && usbi_locale_supported[i][1] == tolower((unsigned char)locale[1])) + break; + } + + if (i == ARRAYSIZE(usbi_locale_supported)) + return LIBUSB_ERROR_NOT_FOUND; + + usbi_error_strings = &usbi_localized_errors[i]; + + return LIBUSB_SUCCESS; +} + +/** \ingroup libusb_misc + * Returns a constant string with a short description of the given error code, + * this description is intended for displaying to the end user and will be in + * the language set by libusb_setlocale(). + * + * The returned string is encoded in UTF-8. + * + * The messages always start with a capital letter and end without any dot. + * The caller must not free() the returned string. + * + * \param errcode the error code whose description is desired + * \returns a short description of the error code in UTF-8 encoding + */ +DEFAULT_VISIBILITY const char * LIBUSB_CALL libusb_strerror(int errcode) +{ + int errcode_index = -errcode; + + if (errcode_index < 0 || errcode_index >= LIBUSB_ERROR_COUNT) { + /* "Other Error", which should always be our last message, is returned */ + errcode_index = LIBUSB_ERROR_COUNT - 1; + } + + return (*usbi_error_strings)[errcode_index]; +} diff --git a/usbaudio/src/main/cpp/external/libusb/libusb/sync.c b/usbaudio/src/main/cpp/external/libusb/libusb/sync.c new file mode 100644 index 000000000..733927d96 --- /dev/null +++ b/usbaudio/src/main/cpp/external/libusb/libusb/sync.c @@ -0,0 +1,342 @@ +/* -*- Mode: C; indent-tabs-mode:t ; c-basic-offset:8 -*- */ +/* + * Synchronous I/O functions for libusb + * Copyright © 2007-2008 Daniel Drake + * Copyright © 2019 Nathan Hjelm + * Copyright © 2019 Google LLC. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "libusbi.h" + +#include +#include + +/** + * @defgroup libusb_syncio Synchronous device I/O + * + * This page documents libusb's synchronous (blocking) API for USB device I/O. + * This interface is easy to use but has some limitations. More advanced users + * may wish to consider using the \ref libusb_asyncio "asynchronous I/O API" instead. + */ + +static void LIBUSB_CALL sync_transfer_cb(struct libusb_transfer *transfer) +{ + usbi_dbg(TRANSFER_CTX(transfer), "actual_length=%d", transfer->actual_length); + + int *completed = transfer->user_data; + *completed = 1; + /* + * Right after setting 'completed', another thread might free the transfer, so don't + * access it beyond this point. The instantiating thread (not necessarily the + * current one) interprets the result and frees the transfer. + */ +} + +static void sync_transfer_wait_for_completion(struct libusb_transfer *transfer) +{ + int r, *completed = transfer->user_data; + struct libusb_context *ctx = HANDLE_CTX(transfer->dev_handle); + + while (!*completed) { + r = libusb_handle_events_completed(ctx, completed); + if (r < 0) { + if (r == LIBUSB_ERROR_INTERRUPTED) + continue; + usbi_err(ctx, "libusb_handle_events failed: %s, cancelling transfer and retrying", + libusb_error_name(r)); + libusb_cancel_transfer(transfer); + continue; + } + if (NULL == transfer->dev_handle) { + /* transfer completion after libusb_close() */ + transfer->status = LIBUSB_TRANSFER_NO_DEVICE; + *completed = 1; + } + } +} + +/** \ingroup libusb_syncio + * Perform a USB control transfer. + * + * The direction of the transfer is inferred from the bmRequestType field of + * the setup packet. + * + * The wValue, wIndex and wLength fields values should be given in host-endian + * byte order. + * + * \param dev_handle a handle for the device to communicate with + * \param bmRequestType the request type field for the setup packet + * \param bRequest the request field for the setup packet + * \param wValue the value field for the setup packet + * \param wIndex the index field for the setup packet + * \param data a suitably-sized data buffer for either input or output + * (depending on direction bits within bmRequestType) + * \param wLength the length field for the setup packet. The data buffer should + * be at least this size. + * \param timeout timeout (in milliseconds) that this function should wait + * before giving up due to no response being received. For an unlimited + * timeout, use value 0. + * \returns on success, the number of bytes actually transferred + * \returns \ref LIBUSB_ERROR_TIMEOUT if the transfer timed out + * \returns \ref LIBUSB_ERROR_PIPE if the control request was not supported by the + * device + * \returns \ref LIBUSB_ERROR_NO_DEVICE if the device has been disconnected + * \returns \ref LIBUSB_ERROR_BUSY if called from event handling context + * \returns \ref LIBUSB_ERROR_INVALID_PARAM if the transfer size is larger than + * the operating system and/or hardware can support (see \ref asynclimits) + * \returns another LIBUSB_ERROR code on other failures + */ +int API_EXPORTED libusb_control_transfer(libusb_device_handle *dev_handle, + uint8_t bmRequestType, uint8_t bRequest, uint16_t wValue, uint16_t wIndex, + unsigned char *data, uint16_t wLength, unsigned int timeout) +{ + struct libusb_transfer *transfer; + unsigned char *buffer; + int completed = 0; + int r; + + if (usbi_handling_events(HANDLE_CTX(dev_handle))) + return LIBUSB_ERROR_BUSY; + + transfer = libusb_alloc_transfer(0); + if (!transfer) + return LIBUSB_ERROR_NO_MEM; + + buffer = malloc(LIBUSB_CONTROL_SETUP_SIZE + wLength); + if (!buffer) { + libusb_free_transfer(transfer); + return LIBUSB_ERROR_NO_MEM; + } + + libusb_fill_control_setup(buffer, bmRequestType, bRequest, wValue, wIndex, + wLength); + if ((bmRequestType & LIBUSB_ENDPOINT_DIR_MASK) == LIBUSB_ENDPOINT_OUT) + memcpy(buffer + LIBUSB_CONTROL_SETUP_SIZE, data, wLength); + + libusb_fill_control_transfer(transfer, dev_handle, buffer, + sync_transfer_cb, &completed, timeout); + transfer->flags = LIBUSB_TRANSFER_FREE_BUFFER; + r = libusb_submit_transfer(transfer); + if (r < 0) { + libusb_free_transfer(transfer); + return r; + } + + sync_transfer_wait_for_completion(transfer); + + if ((bmRequestType & LIBUSB_ENDPOINT_DIR_MASK) == LIBUSB_ENDPOINT_IN) + memcpy(data, libusb_control_transfer_get_data(transfer), + (size_t)transfer->actual_length); + + switch (transfer->status) { + case LIBUSB_TRANSFER_COMPLETED: + r = transfer->actual_length; + break; + case LIBUSB_TRANSFER_TIMED_OUT: + r = LIBUSB_ERROR_TIMEOUT; + break; + case LIBUSB_TRANSFER_STALL: + r = LIBUSB_ERROR_PIPE; + break; + case LIBUSB_TRANSFER_NO_DEVICE: + r = LIBUSB_ERROR_NO_DEVICE; + break; + case LIBUSB_TRANSFER_OVERFLOW: + r = LIBUSB_ERROR_OVERFLOW; + break; + case LIBUSB_TRANSFER_ERROR: + case LIBUSB_TRANSFER_CANCELLED: + r = LIBUSB_ERROR_IO; + break; + default: + usbi_warn(HANDLE_CTX(dev_handle), + "unrecognised status code %d", transfer->status); + r = LIBUSB_ERROR_OTHER; + } + + libusb_free_transfer(transfer); + return r; +} + +static int do_sync_bulk_transfer(struct libusb_device_handle *dev_handle, + unsigned char endpoint, unsigned char *buffer, int length, + int *transferred, unsigned int timeout, unsigned char type) +{ + struct libusb_transfer *transfer; + int completed = 0; + int r; + + if (usbi_handling_events(HANDLE_CTX(dev_handle))) + return LIBUSB_ERROR_BUSY; + + transfer = libusb_alloc_transfer(0); + if (!transfer) + return LIBUSB_ERROR_NO_MEM; + + libusb_fill_bulk_transfer(transfer, dev_handle, endpoint, buffer, length, + sync_transfer_cb, &completed, timeout); + transfer->type = type; + + r = libusb_submit_transfer(transfer); + if (r < 0) { + libusb_free_transfer(transfer); + return r; + } + + sync_transfer_wait_for_completion(transfer); + + if (transferred) { + assert(transfer->actual_length >= 0); + *transferred = transfer->actual_length; + } + + switch (transfer->status) { + case LIBUSB_TRANSFER_COMPLETED: + r = 0; + break; + case LIBUSB_TRANSFER_TIMED_OUT: + r = LIBUSB_ERROR_TIMEOUT; + break; + case LIBUSB_TRANSFER_STALL: + r = LIBUSB_ERROR_PIPE; + break; + case LIBUSB_TRANSFER_OVERFLOW: + r = LIBUSB_ERROR_OVERFLOW; + break; + case LIBUSB_TRANSFER_NO_DEVICE: + r = LIBUSB_ERROR_NO_DEVICE; + break; + case LIBUSB_TRANSFER_ERROR: + case LIBUSB_TRANSFER_CANCELLED: + r = LIBUSB_ERROR_IO; + break; + default: + usbi_warn(HANDLE_CTX(dev_handle), + "unrecognised status code %d", transfer->status); + r = LIBUSB_ERROR_OTHER; + } + + libusb_free_transfer(transfer); + return r; +} + +/** \ingroup libusb_syncio + * Perform a USB bulk transfer. The direction of the transfer is inferred from + * the direction bits of the endpoint address. + * + * For bulk reads, the length field indicates the maximum length of + * data you are expecting to receive. If less data arrives than expected, + * this function will return that data, so be sure to check the + * transferred output parameter. + * + * You should also check the transferred parameter for bulk writes. + * Not all of the data may have been written. + * + * Also check transferred when dealing with a timeout error code. + * libusb may have to split your transfer into a number of chunks to satisfy + * underlying O/S requirements, meaning that the timeout may expire after + * the first few chunks have completed. libusb is careful not to lose any data + * that may have been transferred; do not assume that timeout conditions + * indicate a complete lack of I/O. See \ref asynctimeout for more details. + * + * \param dev_handle a handle for the device to communicate with + * \param endpoint the address of a valid endpoint to communicate with + * \param data a suitably-sized data buffer for either input or output + * (depending on endpoint) + * \param length for bulk writes, the number of bytes from data to be sent. for + * bulk reads, the maximum number of bytes to receive into the data buffer. + * \param transferred output location for the number of bytes actually + * transferred. Since version 1.0.21 (\ref LIBUSB_API_VERSION >= 0x01000105), + * it is legal to pass a NULL pointer if you do not wish to receive this + * information. + * \param timeout timeout (in milliseconds) that this function should wait + * before giving up due to no response being received. For an unlimited + * timeout, use value 0. + * + * \returns 0 on success (and populates transferred) + * \returns \ref LIBUSB_ERROR_TIMEOUT if the transfer timed out (and populates + * transferred) + * \returns \ref LIBUSB_ERROR_PIPE if the endpoint halted + * \returns \ref LIBUSB_ERROR_OVERFLOW if the device offered more data, see + * \ref libusb_packetoverflow + * \returns \ref LIBUSB_ERROR_NO_DEVICE if the device has been disconnected + * \returns \ref LIBUSB_ERROR_BUSY if called from event handling context + * \returns \ref LIBUSB_ERROR_INVALID_PARAM if the transfer size is larger than + * the operating system and/or hardware can support (see \ref asynclimits) + * \returns another LIBUSB_ERROR code on other failures + */ +int API_EXPORTED libusb_bulk_transfer(libusb_device_handle *dev_handle, + unsigned char endpoint, unsigned char *data, int length, + int *transferred, unsigned int timeout) +{ + return do_sync_bulk_transfer(dev_handle, endpoint, data, length, + transferred, timeout, LIBUSB_TRANSFER_TYPE_BULK); +} + +/** \ingroup libusb_syncio + * Perform a USB interrupt transfer. The direction of the transfer is inferred + * from the direction bits of the endpoint address. + * + * For interrupt reads, the length field indicates the maximum length + * of data you are expecting to receive. If less data arrives than expected, + * this function will return that data, so be sure to check the + * transferred output parameter. + * + * You should also check the transferred parameter for interrupt + * writes. Not all of the data may have been written. + * + * Also check transferred when dealing with a timeout error code. + * libusb may have to split your transfer into a number of chunks to satisfy + * underlying O/S requirements, meaning that the timeout may expire after + * the first few chunks have completed. libusb is careful not to lose any data + * that may have been transferred; do not assume that timeout conditions + * indicate a complete lack of I/O. See \ref asynctimeout for more details. + * + * The default endpoint bInterval value is used as the polling interval. + * + * \param dev_handle a handle for the device to communicate with + * \param endpoint the address of a valid endpoint to communicate with + * \param data a suitably-sized data buffer for either input or output + * (depending on endpoint) + * \param length for bulk writes, the number of bytes from data to be sent. for + * bulk reads, the maximum number of bytes to receive into the data buffer. + * \param transferred output location for the number of bytes actually + * transferred. Will never be negative. Since version 1.0.21 + * (\ref LIBUSB_API_VERSION >= 0x01000105), it is legal to pass a NULL + * pointer if you do not wish to receive this information. + * \param timeout timeout (in milliseconds) that this function should wait + * before giving up due to no response being received. For an unlimited + * timeout, use value 0. + * + * \returns 0 on success (and populates transferred) + * \returns \ref LIBUSB_ERROR_TIMEOUT if the transfer timed out + * \returns \ref LIBUSB_ERROR_PIPE if the endpoint halted + * \returns \ref LIBUSB_ERROR_OVERFLOW if the device offered more data, see + * \ref libusb_packetoverflow + * \returns \ref LIBUSB_ERROR_NO_DEVICE if the device has been disconnected + * \returns \ref LIBUSB_ERROR_BUSY if called from event handling context + * \returns \ref LIBUSB_ERROR_INVALID_PARAM if the transfer size is larger than + * the operating system and/or hardware can support (see \ref asynclimits) + * \returns another LIBUSB_ERROR code on other error + */ +int API_EXPORTED libusb_interrupt_transfer(libusb_device_handle *dev_handle, + unsigned char endpoint, unsigned char *data, int length, + int *transferred, unsigned int timeout) +{ + return do_sync_bulk_transfer(dev_handle, endpoint, data, length, + transferred, timeout, LIBUSB_TRANSFER_TYPE_INTERRUPT); +} diff --git a/usbaudio/src/main/cpp/external/libusb/libusb/version.h b/usbaudio/src/main/cpp/external/libusb/libusb/version.h new file mode 100644 index 000000000..2a9265832 --- /dev/null +++ b/usbaudio/src/main/cpp/external/libusb/libusb/version.h @@ -0,0 +1,18 @@ +/* This file is parsed by m4 and windres and RC.EXE so please keep it simple. */ +#include "version_nano.h" +#ifndef LIBUSB_MAJOR +#define LIBUSB_MAJOR 1 +#endif +#ifndef LIBUSB_MINOR +#define LIBUSB_MINOR 0 +#endif +#ifndef LIBUSB_MICRO +#define LIBUSB_MICRO 30 +#endif +#ifndef LIBUSB_NANO +#define LIBUSB_NANO 0 +#endif +/* LIBUSB_RC is the release candidate suffix. Should normally be empty. */ +#ifndef LIBUSB_RC +#define LIBUSB_RC "" +#endif diff --git a/usbaudio/src/main/cpp/external/libusb/libusb/version_nano.h b/usbaudio/src/main/cpp/external/libusb/libusb/version_nano.h new file mode 100644 index 000000000..f009126a9 --- /dev/null +++ b/usbaudio/src/main/cpp/external/libusb/libusb/version_nano.h @@ -0,0 +1 @@ +#define LIBUSB_NANO 12037 diff --git a/usbaudio/src/main/cpp/jni_bridge.cpp b/usbaudio/src/main/cpp/jni_bridge.cpp new file mode 100644 index 000000000..a65b9c9d2 --- /dev/null +++ b/usbaudio/src/main/cpp/jni_bridge.cpp @@ -0,0 +1,13 @@ +#include + +#include + +#include "libusb.h" + +extern "C" JNIEXPORT jstring JNICALL +Java_com_theveloper_pixelplay_usbaudio_UsbAudioNative_nativeGetVersion(JNIEnv* env, jobject /*thiz*/) { + const libusb_version* v = libusb_get_version(); + char buf[64]; + std::snprintf(buf, sizeof(buf), "libusb %u.%u.%u", v->major, v->minor, v->micro); + return env->NewStringUTF(buf); +} diff --git a/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/UsbAudioNative.kt b/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/UsbAudioNative.kt new file mode 100644 index 000000000..0c77270af --- /dev/null +++ b/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/UsbAudioNative.kt @@ -0,0 +1,28 @@ +package com.theveloper.pixelplay.usbaudio + +/** + * Thin JNI surface over the native USB Audio Class driver (libusb-based). + * + * The native library is loaded lazily via [ensureLoaded] so that the normal + * (non-exclusive) playback path never pays for it — callers must invoke + * [ensureLoaded] before the first native call. + */ +object UsbAudioNative { + + @Volatile + private var loaded = false + + fun ensureLoaded() { + if (!loaded) { + synchronized(this) { + if (!loaded) { + System.loadLibrary("usbaudio_jni") + loaded = true + } + } + } + } + + /** Returns the version string of the bundled libusb, e.g. "libusb 1.0.30". */ + external fun nativeGetVersion(): String +} From 92e5327eab20e3eae4518fda4790d733c503bc6d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 2 Jul 2026 21:03:49 +0000 Subject: [PATCH 02/17] feat(usb): USB DAC detection, permission flow and remembered devices - UsbDeviceManager (@Singleton): enumerates USB Audio Class devices (AudioStreaming interface class 0x01/0x02) at startup and on ATTACHED/DETACHED broadcasts; owns the UsbManager.requestPermission() round-trip via a mutable, package-explicit PendingIntent and surfaces results as a SharedFlow - UsbDeviceInfo with a stable vendor:product:serial identity key (serial is unreadable pre-permission on API 29+, handled) - Preferences: usb_exclusive_mode_enabled toggle, remembered-devices JSON map (label + auto-resume per device), max-volume warning ack - Manifest: optional android.hardware.usb.host feature - JUnit5 tests for the interface filter, key derivation and the remembered-device serialization Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FZMSPh3TgbRAMqgW4q1p6V --- .gitignore | 1 + app/src/main/AndroidManifest.xml | 5 + .../preferences/UserPreferencesRepository.kt | 30 +++ .../pixelplay/data/usb/UsbDeviceInfo.kt | 50 +++++ .../pixelplay/data/usb/UsbDeviceManager.kt | 174 ++++++++++++++++++ .../pixelplay/data/usb/UsbDeviceInfoTest.kt | 63 +++++++ .../data/usb/UsbDeviceManagerFilterTest.kt | 57 ++++++ 7 files changed, 380 insertions(+) create mode 100644 app/src/main/java/com/theveloper/pixelplay/data/usb/UsbDeviceInfo.kt create mode 100644 app/src/main/java/com/theveloper/pixelplay/data/usb/UsbDeviceManager.kt create mode 100644 app/src/test/java/com/theveloper/pixelplay/data/usb/UsbDeviceInfoTest.kt create mode 100644 app/src/test/java/com/theveloper/pixelplay/data/usb/UsbDeviceManagerFilterTest.kt diff --git a/.gitignore b/.gitignore index 986f70d62..ef7ab5ca8 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ app/release/ wear/build/ wear/release/ shared/build/ +usbaudio/build/ baselineprofile/build/ misc.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1afb6f404..dabfec313 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -35,6 +35,11 @@ + + + = + pref { it[PreferencesKeys.USB_EXCLUSIVE_MODE_ENABLED] ?: false } + + suspend fun setUsbExclusiveModeEnabled(enabled: Boolean) { + dataStore.edit { it[PreferencesKeys.USB_EXCLUSIVE_MODE_ENABLED] = enabled } + } + + /** Remembered USB DACs keyed by [com.theveloper.pixelplay.data.usb.UsbDeviceInfo.key]. */ + val usbRememberedDevicesFlow: Flow> = + pref { decodeJsonPref(it, PreferencesKeys.USB_REMEMBERED_DEVICES, emptyMap()) } + + suspend fun rememberUsbDevice(key: String, device: UsbRememberedDevice) { + editJsonMap(PreferencesKeys.USB_REMEMBERED_DEVICES) { put(key, device) } + } + + suspend fun forgetUsbDevice(key: String) { + editJsonMap(PreferencesKeys.USB_REMEMBERED_DEVICES) { remove(key) } + } + + val usbExclusiveMaxVolumeAckFlow: Flow = + pref { it[PreferencesKeys.USB_EXCLUSIVE_MAX_VOLUME_ACK] ?: false } + + suspend fun setUsbExclusiveMaxVolumeAck(acknowledged: Boolean) { + dataStore.edit { it[PreferencesKeys.USB_EXCLUSIVE_MAX_VOLUME_ACK] = acknowledged } + } + val keepPlayingInBackgroundFlow: Flow = pref { it[PreferencesKeys.KEEP_PLAYING_IN_BACKGROUND] ?: true } diff --git a/app/src/main/java/com/theveloper/pixelplay/data/usb/UsbDeviceInfo.kt b/app/src/main/java/com/theveloper/pixelplay/data/usb/UsbDeviceInfo.kt new file mode 100644 index 000000000..1e320c7c4 --- /dev/null +++ b/app/src/main/java/com/theveloper/pixelplay/data/usb/UsbDeviceInfo.kt @@ -0,0 +1,50 @@ +package com.theveloper.pixelplay.data.usb + +import kotlinx.serialization.Serializable + +/** + * Snapshot of an attached USB audio-capable device, safe to hold in UI state. + * + * [serialNumber] is only readable once USB permission has been granted (the platform throws + * [SecurityException] before that on API 29+), so it may be null for a device the user has not + * yet approved. [key] therefore prefers the serial when present but stays stable without it. + */ +data class UsbDeviceInfo( + /** Platform device path, e.g. `/dev/bus/usb/001/002`. Unique while attached. */ + val deviceName: String, + val productName: String?, + val manufacturerName: String?, + val vendorId: Int, + val productId: Int, + val serialNumber: String?, + val hasPermission: Boolean +) { + val key: String get() = usbDeviceKey(vendorId, productId, serialNumber) + + val displayName: String + get() = productName?.takeIf { it.isNotBlank() } + ?: "USB Audio (%04x:%04x)".format(vendorId, productId) +} + +/** + * Stable identity for a remembered device across attach/detach cycles. + * Serial is best-effort (unreadable pre-permission, missing on some hardware). + */ +internal fun usbDeviceKey(vendorId: Int, productId: Int, serialNumber: String?): String { + val serial = serialNumber?.takeIf { it.isNotBlank() } ?: "?" + return "%04x:%04x:%s".format(vendorId, productId, serial) +} + +/** Per-device settings persisted in DataStore, keyed by [UsbDeviceInfo.key]. */ +@Serializable +data class UsbRememberedDevice( + val label: String, + val autoResume: Boolean = true +) + +sealed interface UsbPermissionResult { + val device: UsbDeviceInfo + + data class Granted(override val device: UsbDeviceInfo) : UsbPermissionResult + data class Denied(override val device: UsbDeviceInfo) : UsbPermissionResult +} diff --git a/app/src/main/java/com/theveloper/pixelplay/data/usb/UsbDeviceManager.kt b/app/src/main/java/com/theveloper/pixelplay/data/usb/UsbDeviceManager.kt new file mode 100644 index 000000000..e1a0c4056 --- /dev/null +++ b/app/src/main/java/com/theveloper/pixelplay/data/usb/UsbDeviceManager.kt @@ -0,0 +1,174 @@ +package com.theveloper.pixelplay.data.usb + +import android.app.PendingIntent +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.UsbDeviceConnection +import android.hardware.usb.UsbManager +import androidx.core.content.ContextCompat +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject +import javax.inject.Singleton +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import timber.log.Timber + +/** + * Tracks attached USB Audio Class devices and owns the USB permission flow. + * + * Detection is broadcast-driven (ATTACHED/DETACHED) plus an enumeration at construction time so + * a DAC that was already plugged in when the app started is found too. Only devices exposing an + * AudioStreaming interface (class 0x01 / subclass 0x02) are listed. + */ +@Singleton +class UsbDeviceManager @Inject constructor( + @ApplicationContext private val context: Context +) { + private val usbManager: UsbManager? = + context.getSystemService(Context.USB_SERVICE) as? UsbManager + + private val _attachedAudioDevices = MutableStateFlow>(emptyList()) + val attachedAudioDevices: StateFlow> = _attachedAudioDevices.asStateFlow() + + private val _permissionEvents = MutableSharedFlow(extraBufferCapacity = 8) + val permissionEvents: SharedFlow = _permissionEvents.asSharedFlow() + + private val receiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + when (intent.action) { + UsbManager.ACTION_USB_DEVICE_ATTACHED, + UsbManager.ACTION_USB_DEVICE_DETACHED -> refreshAttachedDevices() + + ACTION_USB_PERMISSION -> { + val device: UsbDevice? = + intent.getParcelableExtra(UsbManager.EXTRA_DEVICE, UsbDevice::class.java) + val granted = intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false) + // Refresh first so the emitted snapshot carries hasPermission/serial info. + refreshAttachedDevices() + if (device != null) { + val info = device.toInfo() + Timber.tag(TAG).i("USB permission %s for %s", if (granted) "granted" else "denied", info.key) + _permissionEvents.tryEmit( + if (granted) UsbPermissionResult.Granted(info) + else UsbPermissionResult.Denied(info) + ) + } + } + } + } + } + + init { + if (usbManager != null) { + val filter = IntentFilter().apply { + addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED) + addAction(UsbManager.ACTION_USB_DEVICE_DETACHED) + addAction(ACTION_USB_PERMISSION) + } + // NOT_EXPORTED still receives system broadcasts; the permission action is only ever + // sent by our own PendingIntent (explicit package below). + ContextCompat.registerReceiver(context, receiver, filter, ContextCompat.RECEIVER_NOT_EXPORTED) + refreshAttachedDevices() + } else { + Timber.tag(TAG).w("UsbManager unavailable; USB exclusive mode disabled") + } + } + + /** Shows the system USB permission dialog; result arrives on [permissionEvents]. */ + fun requestPermission(device: UsbDeviceInfo) { + val manager = usbManager ?: return + val raw = findRawDevice(device) ?: run { + Timber.tag(TAG).w("requestPermission: %s no longer attached", device.key) + _permissionEvents.tryEmit(UsbPermissionResult.Denied(device)) + return + } + if (manager.hasPermission(raw)) { + _permissionEvents.tryEmit(UsbPermissionResult.Granted(raw.toInfo())) + return + } + val intent = Intent(ACTION_USB_PERMISSION).setPackage(context.packageName) + // FLAG_MUTABLE: the system fills in EXTRA_DEVICE/EXTRA_PERMISSION_GRANTED. + val pendingIntent = PendingIntent.getBroadcast( + context, + device.key.hashCode(), + intent, + PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + ) + manager.requestPermission(raw, pendingIntent) + } + + fun hasPermission(device: UsbDeviceInfo): Boolean { + val manager = usbManager ?: return false + val raw = findRawDevice(device) ?: return false + return manager.hasPermission(raw) + } + + /** + * Opens the device, yielding the file descriptor holder the native driver wraps. + * Returns null when the device is gone or permission is missing. + */ + fun openConnection(device: UsbDeviceInfo): UsbDeviceConnection? { + val manager = usbManager ?: return null + val raw = findRawDevice(device) ?: return null + if (!manager.hasPermission(raw)) return null + return manager.openDevice(raw) + } + + fun refreshAttachedDevices() { + val manager = usbManager ?: return + val devices = manager.deviceList.values + .filter(::hasAudioStreamingInterface) + .map { it.toInfo() } + .sortedBy { it.deviceName } + _attachedAudioDevices.value = devices + } + + private fun findRawDevice(device: UsbDeviceInfo): UsbDevice? = + usbManager?.deviceList?.values?.firstOrNull { it.deviceName == device.deviceName } + + private fun UsbDevice.toInfo(): UsbDeviceInfo { + val permitted = usbManager?.hasPermission(this) == true + return UsbDeviceInfo( + deviceName = deviceName, + productName = productName, + manufacturerName = manufacturerName, + vendorId = vendorId, + productId = productId, + // Reading the serial without permission throws on API 29+. + serialNumber = if (permitted) runCatching { serialNumber }.getOrNull() else null, + hasPermission = permitted + ) + } + + companion object { + private const val TAG = "UsbDeviceManager" + const val ACTION_USB_PERMISSION = "com.theveloper.pixelplay.USB_PERMISSION" + } +} + +/** + * True when the device exposes a USB Audio Class AudioStreaming interface — i.e. it can carry + * an audio stream, as opposed to audio-class control-only or HID/storage composites. + */ +internal fun hasAudioStreamingInterface(device: UsbDevice): Boolean { + for (i in 0 until device.interfaceCount) { + val itf = device.getInterface(i) + if (itf.interfaceClass == UsbConstants.USB_CLASS_AUDIO && + itf.interfaceSubclass == USB_SUBCLASS_AUDIOSTREAMING + ) { + return true + } + } + return false +} + +/** USB Audio Class interface subclass code for AudioStreaming (UAC1 §A.2 / UAC2 §A.5). */ +internal const val USB_SUBCLASS_AUDIOSTREAMING = 0x02 diff --git a/app/src/test/java/com/theveloper/pixelplay/data/usb/UsbDeviceInfoTest.kt b/app/src/test/java/com/theveloper/pixelplay/data/usb/UsbDeviceInfoTest.kt new file mode 100644 index 000000000..58d3ab766 --- /dev/null +++ b/app/src/test/java/com/theveloper/pixelplay/data/usb/UsbDeviceInfoTest.kt @@ -0,0 +1,63 @@ +package com.theveloper.pixelplay.data.usb + +import com.google.common.truth.Truth.assertThat +import kotlinx.serialization.json.Json +import org.junit.jupiter.api.Test + +class UsbDeviceInfoTest { + + @Test + fun `key uses zero-padded hex ids and serial`() { + assertThat(usbDeviceKey(0x2D87, 0x0001, "ABC123")).isEqualTo("2d87:0001:ABC123") + } + + @Test + fun `key tolerates missing serial`() { + assertThat(usbDeviceKey(0x20B1, 0x300A, null)).isEqualTo("20b1:300a:?") + } + + @Test + fun `key treats blank serial as missing`() { + assertThat(usbDeviceKey(0x20B1, 0x300A, " ")).isEqualTo("20b1:300a:?") + } + + @Test + fun `displayName falls back to ids when product name missing`() { + val info = UsbDeviceInfo( + deviceName = "/dev/bus/usb/001/002", + productName = null, + manufacturerName = null, + vendorId = 0x20B1, + productId = 0x000A, + serialNumber = null, + hasPermission = false + ) + assertThat(info.displayName).isEqualTo("USB Audio (20b1:000a)") + } + + @Test + fun `displayName prefers product name`() { + val info = UsbDeviceInfo( + deviceName = "/dev/bus/usb/001/002", + productName = "D10s", + manufacturerName = "Topping", + vendorId = 0x152A, + productId = 0x8750, + serialNumber = "S1", + hasPermission = true + ) + assertThat(info.displayName).isEqualTo("D10s") + } + + @Test + fun `remembered device round-trips through json`() { + val json = Json { ignoreUnknownKeys = true } + val original = mapOf( + "152a:8750:S1" to UsbRememberedDevice(label = "Topping D10s", autoResume = true), + "20b1:000a:?" to UsbRememberedDevice(label = "Old dongle", autoResume = false) + ) + val decoded: Map = + json.decodeFromString(json.encodeToString(original)) + assertThat(decoded).isEqualTo(original) + } +} diff --git a/app/src/test/java/com/theveloper/pixelplay/data/usb/UsbDeviceManagerFilterTest.kt b/app/src/test/java/com/theveloper/pixelplay/data/usb/UsbDeviceManagerFilterTest.kt new file mode 100644 index 000000000..894845657 --- /dev/null +++ b/app/src/test/java/com/theveloper/pixelplay/data/usb/UsbDeviceManagerFilterTest.kt @@ -0,0 +1,57 @@ +package com.theveloper.pixelplay.data.usb + +import android.hardware.usb.UsbDevice +import android.hardware.usb.UsbInterface +import com.google.common.truth.Truth.assertThat +import io.mockk.every +import io.mockk.mockk +import org.junit.jupiter.api.Test + +class UsbDeviceManagerFilterTest { + + private fun device(vararg interfaces: Pair): UsbDevice { + val mocks = interfaces.map { (cls, subCls) -> + mockk { + every { interfaceClass } returns cls + every { interfaceSubclass } returns subCls + } + } + return mockk { + every { interfaceCount } returns mocks.size + mocks.forEachIndexed { index, itf -> every { getInterface(index) } returns itf } + } + } + + @Test + fun `uac dac with control and streaming interfaces matches`() { + // Typical UAC2 DAC: AudioControl (1,1) + AudioStreaming (1,2) + val dac = device(1 to 1, 1 to 2) + assertThat(hasAudioStreamingInterface(dac)).isTrue() + } + + @Test + fun `mass storage device does not match`() { + val msc = device(8 to 6) + assertThat(hasAudioStreamingInterface(msc)).isFalse() + } + + @Test + fun `audio control only device does not match`() { + // Control interface without streaming (e.g. HID volume knob with audio control) + val controlOnly = device(1 to 1) + assertThat(hasAudioStreamingInterface(controlOnly)).isFalse() + } + + @Test + fun `composite audio plus hid matches`() { + // DACs commonly expose HID (3,0) for buttons alongside audio + val composite = device(1 to 1, 1 to 2, 3 to 0) + assertThat(hasAudioStreamingInterface(composite)).isTrue() + } + + @Test + fun `device with no interfaces does not match`() { + val empty = device() + assertThat(hasAudioStreamingInterface(empty)).isFalse() + } +} From e1d95ff223e409d67113b5f06b0f0da077e261b4 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 2 Jul 2026 21:03:49 +0000 Subject: [PATCH 03/17] feat(usb): UAC1/UAC2 descriptor parsing and bit-perfect format negotiation Pure-Kotlin, hardware-independent core of the USB audio driver: - UsbDescriptorParser walks getRawDescriptors() blobs into a UacTopology: terminals, clock sources, feature units (volume/mute capabilities) and every playback alt setting with wire format, iso endpoint sync type and explicit-feedback endpoint. Handles UAC1 (rates in Type I format descriptors) and UAC2 (clock topology); returns sealed ParseResult and never throws on malformed input - UacCapabilityProber resolves UAC2 sample rates via the clock source's RANGE request through an injected control-transfer function (plain EP0 class request, testable with canned payloads) - FormatNegotiator picks alt setting + rate for a source format with the priority: no downmix > no resample > no depth reduction > tightest depth > same-clock-family rate; float32 input negotiates as 24-bit (lossless for int sources up to 24-bit) - 30 JUnit5 tests: byte-exact UAC1 dongle / UAC2 async DAC / composite blobs, truncation/garbage robustness, RANGE decoding, negotiation table Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FZMSPh3TgbRAMqgW4q1p6V --- .../usbaudio/descriptor/UacCapabilities.kt | 52 +++ .../descriptor/UacCapabilityProber.kt | 146 +++++++ .../usbaudio/descriptor/UacTopology.kt | 119 ++++++ .../descriptor/UsbDescriptorParser.kt | 379 ++++++++++++++++++ .../usbaudio/negotiation/FormatNegotiator.kt | 113 ++++++ .../usbaudio/descriptor/TestDescriptors.kt | 194 +++++++++ .../descriptor/UacCapabilityProberTest.kt | 97 +++++ .../descriptor/UsbDescriptorParserTest.kt | 150 +++++++ .../negotiation/FormatNegotiatorTest.kt | 146 +++++++ 9 files changed, 1396 insertions(+) create mode 100644 usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/descriptor/UacCapabilities.kt create mode 100644 usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/descriptor/UacCapabilityProber.kt create mode 100644 usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/descriptor/UacTopology.kt create mode 100644 usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/descriptor/UsbDescriptorParser.kt create mode 100644 usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/negotiation/FormatNegotiator.kt create mode 100644 usbaudio/src/test/java/com/theveloper/pixelplay/usbaudio/descriptor/TestDescriptors.kt create mode 100644 usbaudio/src/test/java/com/theveloper/pixelplay/usbaudio/descriptor/UacCapabilityProberTest.kt create mode 100644 usbaudio/src/test/java/com/theveloper/pixelplay/usbaudio/descriptor/UsbDescriptorParserTest.kt create mode 100644 usbaudio/src/test/java/com/theveloper/pixelplay/usbaudio/negotiation/FormatNegotiatorTest.kt diff --git a/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/descriptor/UacCapabilities.kt b/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/descriptor/UacCapabilities.kt new file mode 100644 index 000000000..3c26b8683 --- /dev/null +++ b/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/descriptor/UacCapabilities.kt @@ -0,0 +1,52 @@ +package com.theveloper.pixelplay.usbaudio.descriptor + +/** + * Fully resolved playback capabilities of a DAC: what [UacCapabilityProber] hands to the + * format negotiator once descriptor parsing and (for UAC2) clock-rate probing are done. + */ +data class UacCapabilities( + val version: UacVersion, + val controlInterfaceNumber: Int, + val formats: List, + /** Hardware volume via a UAC feature unit, or null → fixed line-level output. */ + val volume: VolumeCapability?, + /** Hook for DSD-over-PCM support detection (stretch goal); always NONE for now. */ + val dsdSupport: DsdSupport = DsdSupport.NONE +) { + /** Every sample rate the device can play, across all formats. */ + val allSampleRatesHz: List + get() = formats.flatMap { it.sampleRatesHz }.distinct().sorted() + + /** Every bit resolution the device offers. */ + val allBitResolutions: List + get() = formats.map { it.bitResolution }.distinct().sorted() +} + +enum class DsdSupport { NONE, DOP } + +/** One playable configuration: an alt setting with its resolved rates. */ +data class FormatCandidate( + val interfaceNumber: Int, + val altSetting: Int, + val channels: Int, + /** Bytes per sample on the wire (2, 3 or 4). */ + val subslotBytes: Int, + /** Meaningful bits (16, 24 or 32). */ + val bitResolution: Int, + val sampleRatesHz: List, + val endpointAddress: Int, + val maxPacketSize: Int, + val intervalCode: Int, + val syncType: EndpointSyncType, + val feedbackEndpointAddress: Int?, + /** UAC2 clock source to program for this stream; null on UAC1. */ + val clockSourceId: Int?, + /** UAC1 endpoint supports SET_CUR sampling frequency. */ + val uac1SampleRateControl: Boolean +) + +data class VolumeCapability( + val featureUnitId: Int, + val hasMasterVolume: Boolean, + val hasMasterMute: Boolean +) diff --git a/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/descriptor/UacCapabilityProber.kt b/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/descriptor/UacCapabilityProber.kt new file mode 100644 index 000000000..dbce3f983 --- /dev/null +++ b/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/descriptor/UacCapabilityProber.kt @@ -0,0 +1,146 @@ +package com.theveloper.pixelplay.usbaudio.descriptor + +/** + * Control-transfer abstraction mirroring `UsbDeviceConnection.controlTransfer(...)` so the + * prober can run against the Java USB API in production and canned payloads in tests. + * Returns the number of bytes transferred, or a negative value on failure. + */ +fun interface UsbControlTransfer { + operator fun invoke(requestType: Int, request: Int, value: Int, index: Int, buffer: ByteArray): Int +} + +/** + * Turns a parsed [UacTopology] into [UacCapabilities]. + * + * UAC1 devices advertise their sample rates directly in the Type I format descriptors. + * UAC2 devices instead expose them through a RANGE request on the clock source's sampling + * frequency control — a plain EP0 class request that needs no claimed interface, so this + * runs from Kotlin before the native driver ever touches the device. + */ +object UacCapabilityProber { + + /** Rates worth reporting; arbitrary in-between values are unusable for playback anyway. */ + internal val STANDARD_RATES = listOf( + 8000, 11025, 16000, 22050, 32000, 44100, 48000, 64000, + 88200, 96000, 176400, 192000, 352800, 384000, 705600, 768000 + ) + + // UAC2 class request codes (spec §5.2.2) + private const val REQ_CUR = 0x01 + private const val REQ_RANGE = 0x02 + + /** Clock source control selector: sampling frequency (UAC2 §A.17.1). */ + private const val CS_SAM_FREQ_CONTROL = 0x01 + + /** bmRequestType: device-to-host | class | interface. */ + private const val REQUEST_TYPE_AC_GET = 0xA1 + + fun probe(topology: UacTopology, controlTransfer: UsbControlTransfer): UacCapabilities { + val formats = topology.playbackAltSettings.mapNotNull { alt -> + val rates = when (topology.version) { + UacVersion.UAC1 -> uac1Rates(alt) + UacVersion.UAC2 -> uac2Rates(topology, alt, controlTransfer) + } + if (rates.isEmpty()) return@mapNotNull null + FormatCandidate( + interfaceNumber = alt.interfaceNumber, + altSetting = alt.altSetting, + channels = alt.channels, + subslotBytes = alt.subslotBytes, + bitResolution = alt.bitResolution, + sampleRatesHz = rates, + endpointAddress = alt.dataEndpoint.address, + maxPacketSize = alt.dataEndpoint.maxPacketSize, + intervalCode = alt.dataEndpoint.intervalCode, + syncType = alt.dataEndpoint.syncType, + feedbackEndpointAddress = alt.feedbackEndpoint?.address, + clockSourceId = topology.clockSourceFor(alt)?.id, + uac1SampleRateControl = alt.uac1SampleRateControl + ) + } + + return UacCapabilities( + version = topology.version, + controlInterfaceNumber = topology.controlInterfaceNumber, + formats = formats, + volume = resolveVolume(topology) + ) + } + + private fun resolveVolume(topology: UacTopology): VolumeCapability? { + val alt = topology.playbackAltSettings.firstOrNull() ?: return null + val unit = topology.featureUnitFor(alt) ?: return null + if (0 !in unit.volumeChannels) return null + return VolumeCapability( + featureUnitId = unit.id, + hasMasterVolume = true, + hasMasterMute = 0 in unit.muteChannels + ) + } + + private fun uac1Rates(alt: StreamingAltSetting): List = when { + alt.sampleRatesHz.isNotEmpty() -> alt.sampleRatesHz.sorted() + alt.continuousRateRange != null -> STANDARD_RATES.filter { it in alt.continuousRateRange } + else -> emptyList() + } + + private fun uac2Rates( + topology: UacTopology, + alt: StreamingAltSetting, + controlTransfer: UsbControlTransfer + ): List { + val clock = topology.clockSourceFor(alt) ?: return emptyList() + val index = (clock.id shl 8) or topology.controlInterfaceNumber + val value = CS_SAM_FREQ_CONTROL shl 8 + + // First learn the subrange count, then fetch the full layout: + // wNumSubRanges + N * (dMIN, dMAX, dRES). + val countBuffer = ByteArray(2) + val countRead = controlTransfer(REQUEST_TYPE_AC_GET, REQ_RANGE, value, index, countBuffer) + if (countRead < 2) return emptyList() + val subRanges = u16(countBuffer, 0) + if (subRanges <= 0 || subRanges > 256) return emptyList() + + val buffer = ByteArray(2 + subRanges * 12) + val read = controlTransfer(REQUEST_TYPE_AC_GET, REQ_RANGE, value, index, buffer) + if (read < buffer.size) return emptyList() + + val rates = sortedSetOf() + for (i in 0 until subRanges) { + val base = 2 + i * 12 + val min = u32(buffer, base) + val max = u32(buffer, base + 4) + val res = u32(buffer, base + 8) + if (min <= 0 || max < min) continue + when { + min == max -> if (min <= Int.MAX_VALUE) rates += min.toInt() + res <= 0L -> STANDARD_RATES.filterTo(rates) { it >= min && it <= max } + else -> STANDARD_RATES.filterTo(rates) { it >= min && it <= max && (it - min) % res == 0L } + } + } + return rates.toList() + } + + /** Reads the clock's current rate (diagnostics; the driver programs it explicitly anyway). */ + fun currentRateHz( + topology: UacTopology, + clockSourceId: Int, + controlTransfer: UsbControlTransfer + ): Int? { + val index = (clockSourceId shl 8) or topology.controlInterfaceNumber + val buffer = ByteArray(4) + val read = controlTransfer(REQUEST_TYPE_AC_GET, REQ_CUR, CS_SAM_FREQ_CONTROL shl 8, index, buffer) + if (read < 4) return null + val rate = u32(buffer, 0) + return if (rate in 1..Int.MAX_VALUE.toLong()) rate.toInt() else null + } + + private fun u16(bytes: ByteArray, offset: Int): Int = + (bytes[offset].toInt() and 0xFF) or ((bytes[offset + 1].toInt() and 0xFF) shl 8) + + private fun u32(bytes: ByteArray, offset: Int): Long = + (bytes[offset].toLong() and 0xFF) or + ((bytes[offset + 1].toLong() and 0xFF) shl 8) or + ((bytes[offset + 2].toLong() and 0xFF) shl 16) or + ((bytes[offset + 3].toLong() and 0xFF) shl 24) +} diff --git a/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/descriptor/UacTopology.kt b/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/descriptor/UacTopology.kt new file mode 100644 index 000000000..2866344a5 --- /dev/null +++ b/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/descriptor/UacTopology.kt @@ -0,0 +1,119 @@ +package com.theveloper.pixelplay.usbaudio.descriptor + +/** + * Model of the audio function of a USB device, extracted from its raw configuration + * descriptors. Pure data — no Android types — so everything here is JVM unit-testable. + */ + +enum class UacVersion { UAC1, UAC2 } + +enum class EndpointSyncType { NONE, ASYNCHRONOUS, ADAPTIVE, SYNCHRONOUS } + +/** An isochronous endpoint of a streaming alt setting. */ +data class IsoEndpoint( + /** bEndpointAddress including the direction bit (0x80 = IN). */ + val address: Int, + val maxPacketSize: Int, + /** Raw bInterval code: period = 2^(bInterval-1) (micro)frames. */ + val intervalCode: Int, + val syncType: EndpointSyncType, + /** True when bmAttributes usage bits mark this as an explicit feedback endpoint. */ + val isFeedback: Boolean +) { + val isInput: Boolean get() = address and 0x80 != 0 +} + +/** One playback format the DAC offers (a non-zero alt setting of an AS interface). */ +data class StreamingAltSetting( + val interfaceNumber: Int, + val altSetting: Int, + /** bTerminalLink from AS_GENERAL — the terminal this stream feeds. */ + val terminalLink: Int, + val channels: Int, + /** Bytes each sample occupies on the wire (bSubframeSize / bSubslotSize). */ + val subslotBytes: Int, + /** Meaningful bits within the subslot (bBitResolution). */ + val bitResolution: Int, + /** + * Discrete rates from the descriptor. UAC1 Type I carries them here; UAC2 leaves this + * empty — rates live behind a clock-source RANGE request (see UacCapabilityProber). + */ + val sampleRatesHz: List, + /** UAC1 continuous-range form (bSamFreqType == 0), null otherwise. */ + val continuousRateRange: IntRange?, + /** The isochronous data OUT endpoint of this alt setting. */ + val dataEndpoint: IsoEndpoint, + /** Explicit feedback IN endpoint, when the data endpoint is asynchronous. */ + val feedbackEndpoint: IsoEndpoint?, + /** UAC1 EP_GENERAL bmAttributes bit 0 — sampling-frequency control on the endpoint. */ + val uac1SampleRateControl: Boolean +) + +/** UAC2 clock source entity (CLOCK_SOURCE descriptor). */ +data class ClockSource( + val id: Int, + val attributes: Int, + val controls: Int +) { + /** Sampling-frequency control is host-programmable (bmControls bits 0-1 == 0b11). */ + val frequencyProgrammable: Boolean get() = controls and 0x03 == 0x03 +} + +/** Feature unit with per-channel mute/volume capabilities (channel 0 = master). */ +data class FeatureUnit( + val id: Int, + val sourceId: Int, + /** Channel indices (0 = master) whose VOLUME control is host-programmable. */ + val volumeChannels: List, + /** Channel indices (0 = master) whose MUTE control is host-programmable. */ + val muteChannels: List +) + +data class AudioTerminal( + val id: Int, + val terminalType: Int, + /** bSourceID for output terminals, null for input terminals. */ + val sourceId: Int?, + /** UAC2 bCSourceID, null on UAC1. */ + val clockSourceId: Int?, + val isInput: Boolean +) { + companion object { + /** wTerminalType for a USB streaming terminal (the host-facing side). */ + const val TYPE_USB_STREAMING = 0x0101 + } +} + +/** Everything the app needs to know about a device's audio function. */ +data class UacTopology( + val version: UacVersion, + val controlInterfaceNumber: Int, + val terminals: List, + val clockSources: List, + val featureUnits: List, + /** Playback alt settings (iso OUT), across all AudioStreaming interfaces. */ + val playbackAltSettings: List +) { + /** + * The clock source feeding [alt], resolved through its terminal link (UAC2 only). + */ + fun clockSourceFor(alt: StreamingAltSetting): ClockSource? { + if (version != UacVersion.UAC2) return null + val terminal = terminals.firstOrNull { it.id == alt.terminalLink } ?: return null + val clockId = terminal.clockSourceId ?: return null + return clockSources.firstOrNull { it.id == clockId } + } + + /** + * Feature unit controlling the playback path of [alt]: the unit fed directly by the + * stream's USB-streaming input terminal, or failing that any unit with a master volume. + */ + fun featureUnitFor(alt: StreamingAltSetting): FeatureUnit? = + featureUnits.firstOrNull { it.sourceId == alt.terminalLink && it.volumeChannels.isNotEmpty() } + ?: featureUnits.firstOrNull { 0 in it.volumeChannels } +} + +sealed interface ParseResult { + data class Success(val topology: UacTopology) : ParseResult + data class Failure(val reason: String) : ParseResult +} diff --git a/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/descriptor/UsbDescriptorParser.kt b/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/descriptor/UsbDescriptorParser.kt new file mode 100644 index 000000000..2b54d1253 --- /dev/null +++ b/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/descriptor/UsbDescriptorParser.kt @@ -0,0 +1,379 @@ +package com.theveloper.pixelplay.usbaudio.descriptor + +/** + * Parses the raw USB descriptor blob (`UsbDeviceConnection.getRawDescriptors()`) into a + * [UacTopology]: the audio-control entities (terminals, clock sources, feature units) and every + * playback alt setting with its wire format and endpoints. + * + * Supports USB Audio Class 1.0 (bcdADC 0x0100) and 2.0 (bcdADC 0x0200). Never throws on + * malformed input — any structural problem yields [ParseResult.Failure]. + */ +object UsbDescriptorParser { + + // Standard descriptor types + private const val DT_CONFIG = 0x02 + private const val DT_INTERFACE = 0x04 + private const val DT_ENDPOINT = 0x05 + private const val DT_CS_INTERFACE = 0x24 + private const val DT_CS_ENDPOINT = 0x25 + + // Audio interface subclasses + private const val SUBCLASS_AUDIOCONTROL = 0x01 + private const val SUBCLASS_AUDIOSTREAMING = 0x02 + private const val CLASS_AUDIO = 0x01 + + // AC class-specific descriptor subtypes (shared numbering between UAC1/UAC2 where noted) + private const val AC_HEADER = 0x01 + private const val AC_INPUT_TERMINAL = 0x02 + private const val AC_OUTPUT_TERMINAL = 0x03 + private const val AC_FEATURE_UNIT = 0x06 + private const val AC2_CLOCK_SOURCE = 0x0A + + // AS class-specific descriptor subtypes + private const val AS_GENERAL = 0x01 + private const val AS_FORMAT_TYPE = 0x02 + + private const val FORMAT_TYPE_I = 0x01 + /** UAC1 wFormatTag / UAC2 bmFormats bit 0: linear PCM. */ + private const val UAC1_FORMAT_PCM = 0x0001 + + fun parse(raw: ByteArray): ParseResult { + val descriptors = splitDescriptors(raw) + ?: return ParseResult.Failure("Malformed descriptor chain") + if (descriptors.none { it.type == DT_CONFIG }) { + return ParseResult.Failure("No configuration descriptor") + } + + var version: UacVersion? = null + var controlInterface: Int? = null + val terminals = mutableListOf() + val clockSources = mutableListOf() + val featureUnits = mutableListOf() + val altSettings = mutableListOf() + + // Walk state: the interface descriptor we are currently "inside". + var currentInterface = -1 + var currentAlt = 0 + var currentClass = -1 + var currentSubclass = -1 + + // Accumulators for the streaming alt setting being assembled. + var asGeneral: AsGeneral? = null + var asFormat: AsFormat? = null + var asDataEndpoint: IsoEndpoint? = null + var asFeedbackEndpoint: IsoEndpoint? = null + var asRateControl = false + + fun flushAltSetting() { + val general = asGeneral + val format = asFormat + val endpoint = asDataEndpoint + if (general != null && format != null && endpoint != null && !endpoint.isInput) { + altSettings += StreamingAltSetting( + interfaceNumber = general.interfaceNumber, + altSetting = general.altSetting, + terminalLink = general.terminalLink, + channels = general.channels ?: format.channels ?: 0, + subslotBytes = format.subslotBytes, + bitResolution = format.bitResolution, + sampleRatesHz = format.discreteRates, + continuousRateRange = format.continuousRange, + dataEndpoint = endpoint, + feedbackEndpoint = asFeedbackEndpoint, + uac1SampleRateControl = asRateControl + ) + } + asGeneral = null + asFormat = null + asDataEndpoint = null + asFeedbackEndpoint = null + asRateControl = false + } + + for (d in descriptors) { + when (d.type) { + DT_INTERFACE -> { + flushAltSetting() + if (d.bytes.size < 9) continue + currentInterface = d.u8(2) + currentAlt = d.u8(3) + currentClass = d.u8(5) + currentSubclass = d.u8(6) + } + + DT_CS_INTERFACE -> when { + currentClass == CLASS_AUDIO && currentSubclass == SUBCLASS_AUDIOCONTROL -> { + when (d.u8(2)) { + AC_HEADER -> { + if (d.bytes.size < 5) continue + version = when (d.u16(3)) { + 0x0100 -> UacVersion.UAC1 + 0x0200 -> UacVersion.UAC2 + else -> version + } + controlInterface = currentInterface + } + + AC_INPUT_TERMINAL -> { + if (d.bytes.size < 8) continue + terminals += AudioTerminal( + id = d.u8(3), + terminalType = d.u16(4), + sourceId = null, + clockSourceId = if (version == UacVersion.UAC2 && d.bytes.size > 7) d.u8(7) else null, + isInput = true + ) + } + + AC_OUTPUT_TERMINAL -> { + if (d.bytes.size < 9) continue + terminals += AudioTerminal( + id = d.u8(3), + terminalType = d.u16(4), + sourceId = d.u8(7), + clockSourceId = if (version == UacVersion.UAC2 && d.bytes.size > 8) d.u8(8) else null, + isInput = false + ) + } + + AC_FEATURE_UNIT -> parseFeatureUnit(d, version)?.let { featureUnits += it } + + AC2_CLOCK_SOURCE -> { + if (version == UacVersion.UAC2 && d.bytes.size >= 8) { + clockSources += ClockSource( + id = d.u8(3), + attributes = d.u8(4), + controls = d.u8(5) + ) + } + } + } + } + + currentClass == CLASS_AUDIO && currentSubclass == SUBCLASS_AUDIOSTREAMING -> { + when (d.u8(2)) { + AS_GENERAL -> { + asGeneral = parseAsGeneral(d, version, currentInterface, currentAlt) + } + + AS_FORMAT_TYPE -> { + asFormat = parseFormatType(d, version) + } + } + } + } + + DT_ENDPOINT -> { + if (currentClass != CLASS_AUDIO || currentSubclass != SUBCLASS_AUDIOSTREAMING) continue + if (d.bytes.size < 7) continue + val attributes = d.u8(3) + if (attributes and 0x03 != 0x01) continue // isochronous only + val endpoint = IsoEndpoint( + address = d.u8(2), + maxPacketSize = d.u16(4), + intervalCode = d.u8(6), + syncType = when ((attributes shr 2) and 0x03) { + 1 -> EndpointSyncType.ASYNCHRONOUS + 2 -> EndpointSyncType.ADAPTIVE + 3 -> EndpointSyncType.SYNCHRONOUS + else -> EndpointSyncType.NONE + }, + isFeedback = (attributes shr 4) and 0x03 == 0x01 + ) + if (endpoint.isFeedback && endpoint.isInput) { + asFeedbackEndpoint = endpoint + } else if (!endpoint.isInput) { + asDataEndpoint = endpoint + } + } + + DT_CS_ENDPOINT -> { + // UAC1 EP_GENERAL: bmAttributes bit 0 = sampling frequency control. + if (currentClass == CLASS_AUDIO && currentSubclass == SUBCLASS_AUDIOSTREAMING && + d.bytes.size >= 4 && d.u8(2) == 0x01 + ) { + asRateControl = d.u8(3) and 0x01 != 0 + } + } + } + } + flushAltSetting() + + val resolvedVersion = version + ?: return ParseResult.Failure("No audio-control header (not a UAC device?)") + val acInterface = controlInterface + ?: return ParseResult.Failure("No audio-control interface") + if (altSettings.isEmpty()) { + return ParseResult.Failure("No playback (iso OUT) alt settings found") + } + + return ParseResult.Success( + UacTopology( + version = resolvedVersion, + controlInterfaceNumber = acInterface, + terminals = terminals, + clockSources = clockSources, + featureUnits = featureUnits, + playbackAltSettings = altSettings + ) + ) + } + + // ─── Class-specific pieces ──────────────────────────────────────────────── + + private class AsGeneral( + val interfaceNumber: Int, + val altSetting: Int, + val terminalLink: Int, + val channels: Int?, + val isPcm: Boolean + ) + + private class AsFormat( + val channels: Int?, + val subslotBytes: Int, + val bitResolution: Int, + val discreteRates: List, + val continuousRange: IntRange? + ) + + private fun parseAsGeneral( + d: Descriptor, + version: UacVersion?, + interfaceNumber: Int, + altSetting: Int + ): AsGeneral? = when (version) { + UacVersion.UAC1 -> { + if (d.bytes.size < 7) null + else AsGeneral( + interfaceNumber = interfaceNumber, + altSetting = altSetting, + terminalLink = d.u8(3), + channels = null, // UAC1 channels come from the format-type descriptor + isPcm = d.u16(5) == UAC1_FORMAT_PCM + ).takeIf { it.isPcm } + } + + UacVersion.UAC2 -> { + if (d.bytes.size < 16) null + else AsGeneral( + interfaceNumber = interfaceNumber, + altSetting = altSetting, + terminalLink = d.u8(3), + channels = d.u8(10), + isPcm = d.u32(6) and 0x1L != 0L + ).takeIf { it.isPcm } + } + + null -> null + } + + private fun parseFormatType(d: Descriptor, version: UacVersion?): AsFormat? = when (version) { + UacVersion.UAC1 -> { + if (d.bytes.size < 8 || d.u8(3) != FORMAT_TYPE_I) null + else { + val freqType = d.u8(7) + var discrete = emptyList() + var continuous: IntRange? = null + if (freqType == 0) { + if (d.bytes.size >= 14) continuous = d.u24(8)..d.u24(11) + } else { + val rates = mutableListOf() + for (i in 0 until freqType) { + val offset = 8 + i * 3 + if (offset + 3 > d.bytes.size) break + rates += d.u24(offset) + } + discrete = rates + } + AsFormat( + channels = d.u8(4), + subslotBytes = d.u8(5), + bitResolution = d.u8(6), + discreteRates = discrete, + continuousRange = continuous + ) + } + } + + UacVersion.UAC2 -> { + if (d.bytes.size < 6 || d.u8(3) != FORMAT_TYPE_I) null + else AsFormat( + channels = null, // UAC2 channels come from AS_GENERAL + subslotBytes = d.u8(4), + bitResolution = d.u8(5), + discreteRates = emptyList(), // behind the clock-source RANGE request + continuousRange = null + ) + } + + null -> null + } + + private fun parseFeatureUnit(d: Descriptor, version: UacVersion?): FeatureUnit? { + return when (version) { + UacVersion.UAC1 -> { + if (d.bytes.size < 7) return null + val controlSize = d.u8(5) + if (controlSize <= 0) return null + val channelCount = (d.bytes.size - 7) / controlSize // includes master (ch 0) + val volume = mutableListOf() + val mute = mutableListOf() + for (ch in 0 until channelCount) { + val offset = 6 + ch * controlSize + if (offset >= d.bytes.size) break + val controls = d.u8(offset) + if (controls and 0x02 != 0) volume += ch + if (controls and 0x01 != 0) mute += ch + } + FeatureUnit(id = d.u8(3), sourceId = d.u8(4), volumeChannels = volume, muteChannels = mute) + } + + UacVersion.UAC2 -> { + if (d.bytes.size < 10) return null + val channelCount = (d.bytes.size - 6) / 4 // includes master (ch 0) + val volume = mutableListOf() + val mute = mutableListOf() + for (ch in 0 until channelCount) { + val offset = 5 + ch * 4 + if (offset + 4 > d.bytes.size) break + val controls = d.u32(offset) + // 2 bits per control: 0b11 = host-programmable. Mute = bits 0-1, volume = 2-3. + if ((controls shr 2) and 0x3L == 0x3L) volume += ch + if (controls and 0x3L == 0x3L) mute += ch + } + FeatureUnit(id = d.u8(3), sourceId = d.u8(4), volumeChannels = volume, muteChannels = mute) + } + + null -> null + } + } + + // ─── Low-level walking ──────────────────────────────────────────────────── + + private class Descriptor(val type: Int, val bytes: ByteArray) { + fun u8(i: Int): Int = bytes[i].toInt() and 0xFF + fun u16(i: Int): Int = u8(i) or (u8(i + 1) shl 8) + fun u24(i: Int): Int = u8(i) or (u8(i + 1) shl 8) or (u8(i + 2) shl 16) + fun u32(i: Int): Long = + u8(i).toLong() or (u8(i + 1).toLong() shl 8) or + (u8(i + 2).toLong() shl 16) or (u8(i + 3).toLong() shl 24) + } + + /** Splits the blob into descriptors by bLength, or null if the chain is inconsistent. */ + private fun splitDescriptors(raw: ByteArray): List? { + val out = mutableListOf() + var offset = 0 + while (offset < raw.size) { + if (offset + 2 > raw.size) return null + val length = raw[offset].toInt() and 0xFF + if (length < 2 || offset + length > raw.size) return null + out += Descriptor( + type = raw[offset + 1].toInt() and 0xFF, + bytes = raw.copyOfRange(offset, offset + length) + ) + offset += length + } + return out + } +} diff --git a/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/negotiation/FormatNegotiator.kt b/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/negotiation/FormatNegotiator.kt new file mode 100644 index 000000000..d8704bde7 --- /dev/null +++ b/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/negotiation/FormatNegotiator.kt @@ -0,0 +1,113 @@ +package com.theveloper.pixelplay.usbaudio.negotiation + +import com.theveloper.pixelplay.usbaudio.descriptor.FormatCandidate +import com.theveloper.pixelplay.usbaudio.descriptor.UacCapabilities + +/** Decoded PCM as it leaves the player pipeline. */ +data class SourceFormat( + val sampleRateHz: Int, + /** Bit depth of the integer representation; for float input pass 24 (see [effectiveBitDepth]). */ + val bitDepth: Int, + val channels: Int, + val isFloat: Boolean +) { + /** + * Float32 out of the decoders carries at most 24 significant bits, and any source that was + * originally an integer of ≤24 bits survives the int→float→int trip exactly — so floats + * negotiate as 24-bit for bit-perfect purposes. + */ + val effectiveBitDepth: Int get() = if (isFloat) 24 else bitDepth +} + +/** What had to happen to the samples on the way to the DAC. */ +data class Conversion( + val resampled: Boolean, + val depthReduced: Boolean, + val downmixed: Boolean +) { + val isBitPerfect: Boolean get() = !resampled && !depthReduced && !downmixed + + companion object { + val BIT_PERFECT = Conversion(resampled = false, depthReduced = false, downmixed = false) + } +} + +data class NegotiatedFormat( + val candidate: FormatCandidate, + val sampleRateHz: Int, + val conversion: Conversion +) + +/** + * Chooses the alt setting + rate for a source format, preferring in order: + * no downmix, no resampling, no depth reduction, tightest depth fit, closest rate. + * Pure function — the full decision table lives in FormatNegotiatorTest. + */ +object FormatNegotiator { + + fun negotiate(source: SourceFormat, caps: UacCapabilities): NegotiatedFormat? { + return caps.formats + .mapNotNull { candidate -> evaluate(source, candidate) } + .minWithOrNull( + compareBy( + { if (it.conversion.downmixed) 1 else 0 }, + { if (it.conversion.resampled) 1 else 0 }, + { if (it.conversion.depthReduced) 1 else 0 }, + // Tightest adequate depth (24-bit source → prefer 24 over 32)… + { depthOverhead(source, it.candidate) }, + // …then the least-destructive rate choice. + { rateDistance(source.sampleRateHz, it.sampleRateHz) }, + // Stable tiebreak. + { it.candidate.altSetting } + ) + ) + } + + private fun evaluate(source: SourceFormat, candidate: FormatCandidate): NegotiatedFormat? { + if (candidate.sampleRatesHz.isEmpty() || candidate.channels <= 0) return null + + val downmixed = source.channels > candidate.channels + val depthReduced = source.effectiveBitDepth > candidate.bitResolution + val (rate, resampled) = chooseRate(source.sampleRateHz, candidate.sampleRatesHz) + + return NegotiatedFormat( + candidate = candidate, + sampleRateHz = rate, + conversion = Conversion(resampled = resampled, depthReduced = depthReduced, downmixed = downmixed) + ) + } + + /** + * Exact rate when available; otherwise the smallest same-family rate above the source + * (44.1 kHz material stays on the 44.1 k clock family whenever possible), then the + * smallest higher rate of any family, then the highest rate below. + */ + internal fun chooseRate(sourceRate: Int, rates: List): Pair { + if (sourceRate in rates) return sourceRate to false + + val sameFamily = rates.filter { sameFamily(it, sourceRate) && it > sourceRate }.minOrNull() + if (sameFamily != null) return sameFamily to true + + val higher = rates.filter { it > sourceRate }.minOrNull() + if (higher != null) return higher to true + + return rates.max() to true + } + + /** 44.1 kHz family (multiples of 11 025) vs 48 kHz family (multiples of 8 000). */ + internal fun sameFamily(a: Int, b: Int): Boolean = + (a % 11_025 == 0) == (b % 11_025 == 0) + + private fun depthOverhead(source: SourceFormat, candidate: FormatCandidate): Int { + val overhead = candidate.bitResolution - source.effectiveBitDepth + // Inadequate depth sorts after any adequate depth; among inadequate, deeper is better. + return if (overhead >= 0) overhead else 1000 - overhead + } + + private fun rateDistance(sourceRate: Int, chosenRate: Int): Long { + if (chosenRate == sourceRate) return 0 + // Upsampling is preferable to downsampling at equal distance. + val distance = (chosenRate - sourceRate).toLong() + return if (distance > 0) distance else 1_000_000_000L - distance + } +} diff --git a/usbaudio/src/test/java/com/theveloper/pixelplay/usbaudio/descriptor/TestDescriptors.kt b/usbaudio/src/test/java/com/theveloper/pixelplay/usbaudio/descriptor/TestDescriptors.kt new file mode 100644 index 000000000..2554f39e6 --- /dev/null +++ b/usbaudio/src/test/java/com/theveloper/pixelplay/usbaudio/descriptor/TestDescriptors.kt @@ -0,0 +1,194 @@ +package com.theveloper.pixelplay.usbaudio.descriptor + +/** + * Builds raw descriptor blobs, byte-exact per the UAC1/UAC2 specs, modeled on `lsusb -v` + * dumps of real hardware. Kept as code (not binary resources) so each field is reviewable. + */ +internal class DescriptorBlob { + private val bytes = mutableListOf() + + /** Appends one descriptor; bLength is derived from the payload automatically. */ + fun descriptor(type: Int, vararg payload: Int): DescriptorBlob { + bytes += (payload.size + 2).toByte() + bytes += type.toByte() + payload.forEach { bytes += it.toByte() } + return this + } + + fun raw(vararg values: Int): DescriptorBlob { + values.forEach { bytes += it.toByte() } + return this + } + + fun build(): ByteArray = bytes.toByteArray() +} + +internal fun lo(v: Int) = v and 0xFF +internal fun hi(v: Int) = (v shr 8) and 0xFF +internal fun rate3(hz: Int) = intArrayOf(hz and 0xFF, (hz shr 8) and 0xFF, (hz shr 16) and 0xFF) + +private fun deviceAndConfigHeader(blob: DescriptorBlob) { + // Standard device descriptor (18 bytes) + blob.descriptor( + 0x01, + 0x00, 0x02, // bcdUSB 2.00 + 0x00, 0x00, 0x00, // class/subclass/protocol (per interface) + 0x40, // bMaxPacketSize0 + 0x2A, 0x15, // idVendor + 0x50, 0x87, // idProduct + 0x00, 0x01, // bcdDevice + 0x01, 0x02, 0x03, // iManufacturer/iProduct/iSerialNumber + 0x01 // bNumConfigurations + ) + // Configuration descriptor (9 bytes; wTotalLength unchecked by the parser) + blob.descriptor( + 0x02, + 0x00, 0x00, // wTotalLength (not validated) + 0x02, // bNumInterfaces + 0x01, // bConfigurationValue + 0x00, // iConfiguration + 0xC0, // bmAttributes + 0x32 // bMaxPower + ) +} + +/** + * UAC2 async DAC modeled on a typical Topping/SMSL XMOS design: + * clock source 0x29 (programmable), feature unit 0x0B with master volume+mute, + * AS interface 1 with alt1 = 16-bit/2ch/subslot2 and alt2 = 32-bit/2ch/subslot4, + * async iso OUT endpoint 0x01 + explicit feedback IN endpoint 0x81. + */ +internal fun uac2AsyncDacDescriptors(): ByteArray { + val blob = DescriptorBlob() + deviceAndConfigHeader(blob) + + // Interface 0 alt 0: AudioControl (class 1, subclass 1, protocol 0x20 = UAC2) + blob.descriptor(0x04, 0x00, 0x00, 0x00, 0x01, 0x01, 0x20, 0x00) + // AC HEADER: bcdADC 2.00 + blob.descriptor(0x24, 0x01, 0x00, 0x02, 0x0A, 0x48, 0x00, 0x00) + // CLOCK_SOURCE id 0x29: internal programmable clock, freq control host-programmable (0b11) + blob.descriptor(0x24, 0x0A, 0x29, 0x03, 0x07, 0x00, 0x00) + // INPUT_TERMINAL id 0x01: USB streaming (0x0101), clock 0x29, 2 channels + blob.descriptor( + 0x24, 0x02, 0x01, lo(0x0101), hi(0x0101), 0x00, 0x29, 0x02, + 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ) + // FEATURE_UNIT id 0x0B fed by terminal 0x01: master mute+volume programmable (0x0000000F) + blob.descriptor( + 0x24, 0x06, 0x0B, 0x01, + 0x0F, 0x00, 0x00, 0x00, // master: mute 0b11, volume 0b11 + 0x00, 0x00, 0x00, 0x00, // ch1 + 0x00, 0x00, 0x00, 0x00, // ch2 + 0x00 + ) + // OUTPUT_TERMINAL id 0x03: headphones (0x0302), source 0x0B, clock 0x29 + blob.descriptor(0x24, 0x03, 0x03, lo(0x0302), hi(0x0302), 0x00, 0x0B, 0x29, 0x00, 0x00, 0x00) + + // Interface 1 alt 0: AudioStreaming, zero-bandwidth + blob.descriptor(0x04, 0x01, 0x00, 0x00, 0x01, 0x02, 0x20, 0x00) + + // Interface 1 alt 1: 16-bit + blob.descriptor(0x04, 0x01, 0x01, 0x02, 0x01, 0x02, 0x20, 0x00) + // AS_GENERAL: terminal link 0x01, FORMAT_TYPE_I, bmFormats PCM, 2 channels + blob.descriptor( + 0x24, 0x01, 0x01, 0x05, 0x01, 0x01, 0x00, 0x00, 0x00, + 0x02, 0x03, 0x00, 0x00, 0x00, 0x00 + ) + // FORMAT_TYPE: subslot 2, resolution 16 + blob.descriptor(0x24, 0x02, 0x01, 0x02, 0x10) + // Iso OUT endpoint 0x01, asynchronous (0x05), 512 bytes, bInterval 1 + blob.descriptor(0x05, 0x01, 0x05, lo(512), hi(512), 0x01) + // CS EP_GENERAL + blob.descriptor(0x25, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00) + // Feedback IN endpoint 0x81 (iso, usage=feedback → 0x11), 4 bytes, bInterval 4 + blob.descriptor(0x05, 0x81, 0x11, 0x04, 0x00, 0x04) + + // Interface 1 alt 2: 32-bit subslot (24/32-bit material) + blob.descriptor(0x04, 0x01, 0x02, 0x02, 0x01, 0x02, 0x20, 0x00) + blob.descriptor( + 0x24, 0x01, 0x01, 0x05, 0x01, 0x01, 0x00, 0x00, 0x00, + 0x02, 0x03, 0x00, 0x00, 0x00, 0x00 + ) + blob.descriptor(0x24, 0x02, 0x01, 0x04, 0x20) + blob.descriptor(0x05, 0x01, 0x05, lo(1024), hi(1024), 0x01) + blob.descriptor(0x25, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00) + blob.descriptor(0x05, 0x81, 0x11, 0x04, 0x00, 0x04) + + return blob.build() +} + +/** + * UAC1 dongle modeled on an Apple-style USB-C adapter: adaptive iso OUT, two alt settings + * (16-bit and 24-bit), discrete 44.1/48 kHz rates in the format descriptors, endpoint + * sampling-frequency control, feature unit with master mute+volume. + */ +internal fun uac1DongleDescriptors(): ByteArray { + val blob = DescriptorBlob() + deviceAndConfigHeader(blob) + + // Interface 0 alt 0: AudioControl (UAC1 protocol 0) + blob.descriptor(0x04, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00) + // AC HEADER: bcdADC 1.00, 1 streaming interface (nr 1) + blob.descriptor(0x24, 0x01, 0x00, 0x01, 0x28, 0x00, 0x01, 0x01) + // INPUT_TERMINAL id 0x01: USB streaming, 2 channels + blob.descriptor(0x24, 0x02, 0x01, lo(0x0101), hi(0x0101), 0x00, 0x02, 0x03, 0x00, 0x00, 0x00) + // FEATURE_UNIT id 0x02, source 0x01, bControlSize 1, master mute+volume (0x03) + blob.descriptor(0x24, 0x06, 0x02, 0x01, 0x01, 0x03, 0x00, 0x00, 0x00) + // OUTPUT_TERMINAL id 0x03: headphones, source = feature unit 0x02 + blob.descriptor(0x24, 0x03, 0x03, lo(0x0302), hi(0x0302), 0x00, 0x02, 0x00) + + // Interface 1 alt 0: AudioStreaming zero-bandwidth + blob.descriptor(0x04, 0x01, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00) + + // Interface 1 alt 1: 16-bit, 44.1/48 + blob.descriptor(0x04, 0x01, 0x01, 0x01, 0x01, 0x02, 0x00, 0x00) + // AS_GENERAL: link 0x01, delay 1, wFormatTag PCM + blob.descriptor(0x24, 0x01, 0x01, 0x01, 0x01, 0x00) + // FORMAT_TYPE I: 2ch, subframe 2, 16-bit, 2 discrete rates + blob.descriptor( + 0x24, 0x02, 0x01, 0x02, 0x02, 0x10, 0x02, + *rate3(44_100), *rate3(48_000) + ) + // 9-byte audio-class iso endpoint: OUT 0x01, adaptive (0x09), 192 bytes, interval 1 + blob.descriptor(0x05, 0x01, 0x09, lo(192), hi(192), 0x01, 0x00, 0x00) + // CS EP_GENERAL: sampling frequency control supported (bit 0) + blob.descriptor(0x25, 0x01, 0x01, 0x00, 0x00, 0x00) + + // Interface 1 alt 2: 24-bit, 44.1/48 + blob.descriptor(0x04, 0x01, 0x02, 0x01, 0x01, 0x02, 0x00, 0x00) + blob.descriptor(0x24, 0x01, 0x01, 0x01, 0x01, 0x00) + blob.descriptor( + 0x24, 0x02, 0x01, 0x02, 0x03, 0x18, 0x02, + *rate3(44_100), *rate3(48_000) + ) + blob.descriptor(0x05, 0x01, 0x09, lo(288), hi(288), 0x01, 0x00, 0x00) + blob.descriptor(0x25, 0x01, 0x01, 0x00, 0x00, 0x00) + + return blob.build() +} + +/** Composite device: HID interface first, then the same UAC1 audio function. */ +internal fun compositeHidPlusUac1Descriptors(): ByteArray { + val blob = DescriptorBlob() + deviceAndConfigHeader(blob) + // Interface 2: HID (class 3) + blob.descriptor(0x04, 0x02, 0x00, 0x01, 0x03, 0x00, 0x00, 0x00) + // HID descriptor (type 0x21) — must be skipped cleanly + blob.descriptor(0x21, 0x11, 0x01, 0x00, 0x01, 0x22, 0x40, 0x00) + // Interrupt IN endpoint — not iso, must be ignored + blob.descriptor(0x05, 0x83, 0x03, 0x08, 0x00, 0x0A) + + val audio = uac1DongleDescriptors() + // Skip device+config header of the second blob (18 + 9 bytes) + return blob.build() + audio.copyOfRange(27, audio.size) +} + +/** A mass-storage-only device: no audio function at all. */ +internal fun massStorageDescriptors(): ByteArray { + val blob = DescriptorBlob() + deviceAndConfigHeader(blob) + blob.descriptor(0x04, 0x00, 0x00, 0x02, 0x08, 0x06, 0x50, 0x00) + blob.descriptor(0x05, 0x81, 0x02, lo(512), hi(512), 0x00) + blob.descriptor(0x05, 0x02, 0x02, lo(512), hi(512), 0x00) + return blob.build() +} diff --git a/usbaudio/src/test/java/com/theveloper/pixelplay/usbaudio/descriptor/UacCapabilityProberTest.kt b/usbaudio/src/test/java/com/theveloper/pixelplay/usbaudio/descriptor/UacCapabilityProberTest.kt new file mode 100644 index 000000000..edb97748a --- /dev/null +++ b/usbaudio/src/test/java/com/theveloper/pixelplay/usbaudio/descriptor/UacCapabilityProberTest.kt @@ -0,0 +1,97 @@ +package com.theveloper.pixelplay.usbaudio.descriptor + +import com.google.common.truth.Truth.assertThat +import org.junit.jupiter.api.Test + +class UacCapabilityProberTest { + + private fun topology(raw: ByteArray): UacTopology = + (UsbDescriptorParser.parse(raw) as ParseResult.Success).topology + + /** Encodes a UAC2 RANGE response: wNumSubRanges + N × (dMIN, dMAX, dRES), little-endian. */ + private fun rangeResponse(vararg subRanges: Triple): ByteArray { + val out = ByteArray(2 + subRanges.size * 12) + out[0] = (subRanges.size and 0xFF).toByte() + out[1] = ((subRanges.size shr 8) and 0xFF).toByte() + subRanges.forEachIndexed { i, (min, max, res) -> + var offset = 2 + i * 12 + for (value in longArrayOf(min, max, res)) { + for (b in 0 until 4) out[offset + b] = ((value shr (8 * b)) and 0xFF).toByte() + offset += 4 + } + } + return out + } + + private fun cannedTransfer(response: ByteArray) = UsbControlTransfer { _, request, value, index, buffer -> + // RANGE (0x02) on the sampling-frequency control of clock 0x29, AC interface 0. + assertThat(request).isEqualTo(0x02) + assertThat(value).isEqualTo(0x0100) + assertThat(index).isEqualTo((0x29 shl 8) or 0x00) + val n = minOf(buffer.size, response.size) + response.copyInto(buffer, endIndex = n) + n + } + + @Test + fun `uac2 discrete subranges resolve to their exact rates`() { + val response = rangeResponse( + Triple(44_100L, 44_100L, 0L), + Triple(48_000L, 48_000L, 0L), + Triple(96_000L, 96_000L, 0L), + Triple(192_000L, 192_000L, 0L), + Triple(768_000L, 768_000L, 0L) + ) + val caps = UacCapabilityProber.probe(topology(uac2AsyncDacDescriptors()), cannedTransfer(response)) + + assertThat(caps.version).isEqualTo(UacVersion.UAC2) + assertThat(caps.formats).hasSize(2) + assertThat(caps.formats[0].sampleRatesHz) + .containsExactly(44_100, 48_000, 96_000, 192_000, 768_000).inOrder() + assertThat(caps.formats[1].bitResolution).isEqualTo(32) + assertThat(caps.volume).isNotNull() + assertThat(caps.volume!!.hasMasterMute).isTrue() + } + + @Test + fun `uac2 continuous subrange intersects the standard rate table`() { + val response = rangeResponse(Triple(44_100L, 192_000L, 0L)) + val caps = UacCapabilityProber.probe(topology(uac2AsyncDacDescriptors()), cannedTransfer(response)) + + assertThat(caps.formats[0].sampleRatesHz) + .containsExactly(44_100, 48_000, 64_000, 88_200, 96_000, 176_400, 192_000).inOrder() + } + + @Test + fun `uac2 stepped subrange honours the resolution`() { + // 44100..176400 step 44100 → 44100, 88200, 176400 (132300 is not a standard rate) + val response = rangeResponse(Triple(44_100L, 176_400L, 44_100L)) + val caps = UacCapabilityProber.probe(topology(uac2AsyncDacDescriptors()), cannedTransfer(response)) + + assertThat(caps.formats[0].sampleRatesHz).containsExactly(44_100, 88_200, 176_400).inOrder() + } + + @Test + fun `uac2 failed control transfer yields no formats rather than throwing`() { + val failing = UsbControlTransfer { _, _, _, _, _ -> -1 } + val caps = UacCapabilityProber.probe(topology(uac2AsyncDacDescriptors()), failing) + assertThat(caps.formats).isEmpty() + } + + @Test + fun `uac1 rates come from descriptors and need no control transfers`() { + val neverCalled = UsbControlTransfer { _, _, _, _, _ -> + throw AssertionError("UAC1 probing must not touch the device") + } + val caps = UacCapabilityProber.probe(topology(uac1DongleDescriptors()), neverCalled) + + assertThat(caps.version).isEqualTo(UacVersion.UAC1) + assertThat(caps.formats).hasSize(2) + assertThat(caps.formats[0].sampleRatesHz).containsExactly(44_100, 48_000).inOrder() + assertThat(caps.formats[0].uac1SampleRateControl).isTrue() + assertThat(caps.formats[1].subslotBytes).isEqualTo(3) + assertThat(caps.allSampleRatesHz).containsExactly(44_100, 48_000).inOrder() + assertThat(caps.allBitResolutions).containsExactly(16, 24).inOrder() + assertThat(caps.volume).isNotNull() + } +} diff --git a/usbaudio/src/test/java/com/theveloper/pixelplay/usbaudio/descriptor/UsbDescriptorParserTest.kt b/usbaudio/src/test/java/com/theveloper/pixelplay/usbaudio/descriptor/UsbDescriptorParserTest.kt new file mode 100644 index 000000000..e9f9a9f6f --- /dev/null +++ b/usbaudio/src/test/java/com/theveloper/pixelplay/usbaudio/descriptor/UsbDescriptorParserTest.kt @@ -0,0 +1,150 @@ +package com.theveloper.pixelplay.usbaudio.descriptor + +import com.google.common.truth.Truth.assertThat +import org.junit.jupiter.api.Test + +class UsbDescriptorParserTest { + + private fun parseOk(raw: ByteArray): UacTopology { + val result = UsbDescriptorParser.parse(raw) + assertThat(result).isInstanceOf(ParseResult.Success::class.java) + return (result as ParseResult.Success).topology + } + + // ─── UAC2 ──────────────────────────────────────────────────────────────── + + @Test + fun `uac2 dac parses version and control interface`() { + val topology = parseOk(uac2AsyncDacDescriptors()) + assertThat(topology.version).isEqualTo(UacVersion.UAC2) + assertThat(topology.controlInterfaceNumber).isEqualTo(0) + } + + @Test + fun `uac2 dac exposes both playback alt settings`() { + val topology = parseOk(uac2AsyncDacDescriptors()) + assertThat(topology.playbackAltSettings).hasSize(2) + + val alt1 = topology.playbackAltSettings[0] + assertThat(alt1.interfaceNumber).isEqualTo(1) + assertThat(alt1.altSetting).isEqualTo(1) + assertThat(alt1.channels).isEqualTo(2) + assertThat(alt1.subslotBytes).isEqualTo(2) + assertThat(alt1.bitResolution).isEqualTo(16) + assertThat(alt1.sampleRatesHz).isEmpty() // UAC2: rates live behind the clock RANGE request + assertThat(alt1.terminalLink).isEqualTo(0x01) + + val alt2 = topology.playbackAltSettings[1] + assertThat(alt2.altSetting).isEqualTo(2) + assertThat(alt2.subslotBytes).isEqualTo(4) + assertThat(alt2.bitResolution).isEqualTo(32) + } + + @Test + fun `uac2 endpoints carry sync type and explicit feedback`() { + val topology = parseOk(uac2AsyncDacDescriptors()) + val alt = topology.playbackAltSettings[0] + assertThat(alt.dataEndpoint.address).isEqualTo(0x01) + assertThat(alt.dataEndpoint.syncType).isEqualTo(EndpointSyncType.ASYNCHRONOUS) + assertThat(alt.dataEndpoint.maxPacketSize).isEqualTo(512) + assertThat(alt.feedbackEndpoint).isNotNull() + assertThat(alt.feedbackEndpoint!!.address).isEqualTo(0x81) + assertThat(alt.feedbackEndpoint!!.isInput).isTrue() + } + + @Test + fun `uac2 clock source resolves through the terminal link`() { + val topology = parseOk(uac2AsyncDacDescriptors()) + assertThat(topology.clockSources).hasSize(1) + val clock = topology.clockSourceFor(topology.playbackAltSettings[0]) + assertThat(clock).isNotNull() + assertThat(clock!!.id).isEqualTo(0x29) + assertThat(clock.frequencyProgrammable).isTrue() + } + + @Test + fun `uac2 feature unit exposes programmable master volume and mute`() { + val topology = parseOk(uac2AsyncDacDescriptors()) + val unit = topology.featureUnitFor(topology.playbackAltSettings[0]) + assertThat(unit).isNotNull() + assertThat(unit!!.id).isEqualTo(0x0B) + assertThat(unit.volumeChannels).contains(0) + assertThat(unit.muteChannels).contains(0) + } + + // ─── UAC1 ──────────────────────────────────────────────────────────────── + + @Test + fun `uac1 dongle parses version, rates and depths from descriptors`() { + val topology = parseOk(uac1DongleDescriptors()) + assertThat(topology.version).isEqualTo(UacVersion.UAC1) + assertThat(topology.playbackAltSettings).hasSize(2) + + val alt16 = topology.playbackAltSettings[0] + assertThat(alt16.bitResolution).isEqualTo(16) + assertThat(alt16.subslotBytes).isEqualTo(2) + assertThat(alt16.channels).isEqualTo(2) + assertThat(alt16.sampleRatesHz).containsExactly(44_100, 48_000).inOrder() + assertThat(alt16.uac1SampleRateControl).isTrue() + assertThat(alt16.dataEndpoint.syncType).isEqualTo(EndpointSyncType.ADAPTIVE) + assertThat(alt16.feedbackEndpoint).isNull() + + val alt24 = topology.playbackAltSettings[1] + assertThat(alt24.bitResolution).isEqualTo(24) + assertThat(alt24.subslotBytes).isEqualTo(3) + } + + @Test + fun `uac1 feature unit with single-byte controls parses master volume`() { + val topology = parseOk(uac1DongleDescriptors()) + val unit = topology.featureUnitFor(topology.playbackAltSettings[0]) + assertThat(unit).isNotNull() + assertThat(unit!!.id).isEqualTo(0x02) + assertThat(unit.volumeChannels).containsExactly(0) + assertThat(unit.muteChannels).containsExactly(0) + } + + // ─── Composite / negative ──────────────────────────────────────────────── + + @Test + fun `composite device with leading hid interface still finds the audio function`() { + val topology = parseOk(compositeHidPlusUac1Descriptors()) + assertThat(topology.version).isEqualTo(UacVersion.UAC1) + assertThat(topology.playbackAltSettings).hasSize(2) + } + + @Test + fun `mass storage device fails with a reason`() { + val result = UsbDescriptorParser.parse(massStorageDescriptors()) + assertThat(result).isInstanceOf(ParseResult.Failure::class.java) + } + + @Test + fun `truncated blob fails without throwing`() { + val full = uac2AsyncDacDescriptors() + for (cut in intArrayOf(1, 5, 17, 30, full.size / 2)) { + val result = UsbDescriptorParser.parse(full.copyOfRange(0, cut)) + assertThat(result).isInstanceOf(ParseResult.Failure::class.java) + } + } + + @Test + fun `garbage bytes fail without throwing`() { + val garbage = ByteArray(64) { (it * 37 + 11).toByte() } + val result = UsbDescriptorParser.parse(garbage) + assertThat(result).isInstanceOf(ParseResult.Failure::class.java) + } + + @Test + fun `zero-length descriptor cannot cause an infinite loop`() { + val raw = byteArrayOf(0x00, 0x00, 0x00, 0x00) + val result = UsbDescriptorParser.parse(raw) + assertThat(result).isInstanceOf(ParseResult.Failure::class.java) + } + + @Test + fun `empty input fails`() { + assertThat(UsbDescriptorParser.parse(ByteArray(0))) + .isInstanceOf(ParseResult.Failure::class.java) + } +} diff --git a/usbaudio/src/test/java/com/theveloper/pixelplay/usbaudio/negotiation/FormatNegotiatorTest.kt b/usbaudio/src/test/java/com/theveloper/pixelplay/usbaudio/negotiation/FormatNegotiatorTest.kt new file mode 100644 index 000000000..4c58a904c --- /dev/null +++ b/usbaudio/src/test/java/com/theveloper/pixelplay/usbaudio/negotiation/FormatNegotiatorTest.kt @@ -0,0 +1,146 @@ +package com.theveloper.pixelplay.usbaudio.negotiation + +import com.google.common.truth.Truth.assertThat +import com.theveloper.pixelplay.usbaudio.descriptor.EndpointSyncType +import com.theveloper.pixelplay.usbaudio.descriptor.FormatCandidate +import com.theveloper.pixelplay.usbaudio.descriptor.UacCapabilities +import com.theveloper.pixelplay.usbaudio.descriptor.UacVersion +import org.junit.jupiter.api.Test + +class FormatNegotiatorTest { + + private fun candidate( + altSetting: Int, + bitResolution: Int, + subslotBytes: Int = (bitResolution + 7) / 8, + channels: Int = 2, + rates: List = listOf(44_100, 48_000, 88_200, 96_000, 176_400, 192_000) + ) = FormatCandidate( + interfaceNumber = 1, + altSetting = altSetting, + channels = channels, + subslotBytes = subslotBytes, + bitResolution = bitResolution, + sampleRatesHz = rates, + endpointAddress = 0x01, + maxPacketSize = 1024, + intervalCode = 1, + syncType = EndpointSyncType.ASYNCHRONOUS, + feedbackEndpointAddress = 0x81, + clockSourceId = 0x29, + uac1SampleRateControl = false + ) + + private fun caps(vararg formats: FormatCandidate) = UacCapabilities( + version = UacVersion.UAC2, + controlInterfaceNumber = 0, + formats = formats.toList(), + volume = null + ) + + private val typicalDac = caps( + candidate(altSetting = 1, bitResolution = 16), + candidate(altSetting = 2, bitResolution = 32, subslotBytes = 4) + ) + + @Test + fun `cd audio on a matching dac is bit perfect at 44100`() { + val result = FormatNegotiator.negotiate(SourceFormat(44_100, 16, 2, isFloat = false), typicalDac)!! + assertThat(result.sampleRateHz).isEqualTo(44_100) + assertThat(result.candidate.bitResolution).isEqualTo(16) + assertThat(result.conversion.isBitPerfect).isTrue() + } + + @Test + fun `hi-res float source picks the 32-bit alt setting bit perfectly`() { + // 24/96 FLAC decoded to float by the FFmpeg renderer + val result = FormatNegotiator.negotiate(SourceFormat(96_000, 32, 2, isFloat = true), typicalDac)!! + assertThat(result.sampleRateHz).isEqualTo(96_000) + assertThat(result.candidate.altSetting).isEqualTo(2) + assertThat(result.conversion.isBitPerfect).isTrue() + } + + @Test + fun `24-bit int source prefers tightest adequate depth`() { + val dac = caps( + candidate(altSetting = 1, bitResolution = 24, subslotBytes = 3), + candidate(altSetting = 2, bitResolution = 32, subslotBytes = 4) + ) + val result = FormatNegotiator.negotiate(SourceFormat(96_000, 24, 2, isFloat = false), dac)!! + assertThat(result.candidate.bitResolution).isEqualTo(24) + assertThat(result.conversion.isBitPerfect).isTrue() + } + + @Test + fun `rate the dac lacks resamples within the same clock family upwards`() { + val dac = caps(candidate(altSetting = 1, bitResolution = 24, rates = listOf(48_000, 88_200, 96_000))) + val result = FormatNegotiator.negotiate(SourceFormat(44_100, 16, 2, isFloat = false), dac)!! + // 44.1k material goes to 88.2k (same family), not 48k (nearer but cross-family) + assertThat(result.sampleRateHz).isEqualTo(88_200) + assertThat(result.conversion.resampled).isTrue() + assertThat(result.conversion.isBitPerfect).isFalse() + } + + @Test + fun `rate above the dac maximum downsamples to the highest supported`() { + val dac = caps(candidate(altSetting = 1, bitResolution = 24, rates = listOf(44_100, 48_000, 96_000))) + val result = FormatNegotiator.negotiate(SourceFormat(192_000, 24, 2, isFloat = false), dac)!! + assertThat(result.sampleRateHz).isEqualTo(96_000) + assertThat(result.conversion.resampled).isTrue() + } + + @Test + fun `24-bit source on a 16-bit-only dac reports depth reduction`() { + val dac = caps(candidate(altSetting = 1, bitResolution = 16)) + val result = FormatNegotiator.negotiate(SourceFormat(96_000, 24, 2, isFloat = false), dac)!! + assertThat(result.candidate.bitResolution).isEqualTo(16) + assertThat(result.conversion.depthReduced).isTrue() + assertThat(result.conversion.resampled).isFalse() + } + + @Test + fun `surround source on a stereo dac reports downmix`() { + val result = FormatNegotiator.negotiate(SourceFormat(48_000, 16, 6, isFloat = false), typicalDac)!! + assertThat(result.conversion.downmixed).isTrue() + assertThat(result.conversion.resampled).isFalse() + } + + @Test + fun `mono source on a stereo dac stays bit perfect`() { + val result = FormatNegotiator.negotiate(SourceFormat(48_000, 16, 1, isFloat = false), typicalDac)!! + assertThat(result.conversion.isBitPerfect).isTrue() + } + + @Test + fun `no formats yields null`() { + assertThat(FormatNegotiator.negotiate(SourceFormat(44_100, 16, 2, isFloat = false), caps())).isNull() + } + + @Test + fun `prefers exact rate over tighter depth`() { + val dac = caps( + candidate(altSetting = 1, bitResolution = 24, rates = listOf(48_000)), + candidate(altSetting = 2, bitResolution = 32, subslotBytes = 4, rates = listOf(44_100, 48_000)) + ) + val result = FormatNegotiator.negotiate(SourceFormat(44_100, 24, 2, isFloat = false), dac)!! + assertThat(result.candidate.altSetting).isEqualTo(2) + assertThat(result.conversion.isBitPerfect).isTrue() + } + + @Test + fun `chooseRate helper table`() { + val rates = listOf(44_100, 48_000, 88_200, 96_000) + assertThat(FormatNegotiator.chooseRate(44_100, rates)).isEqualTo(44_100 to false) + assertThat(FormatNegotiator.chooseRate(22_050, rates)).isEqualTo(44_100 to true) + assertThat(FormatNegotiator.chooseRate(176_400, rates)).isEqualTo(96_000 to true) + assertThat(FormatNegotiator.chooseRate(32_000, rates)).isEqualTo(48_000 to true) + } + + @Test + fun `family helper distinguishes 44k1 and 48k families`() { + assertThat(FormatNegotiator.sameFamily(44_100, 88_200)).isTrue() + assertThat(FormatNegotiator.sameFamily(48_000, 96_000)).isTrue() + assertThat(FormatNegotiator.sameFamily(44_100, 48_000)).isFalse() + assertThat(FormatNegotiator.sameFamily(22_050, 176_400)).isTrue() + } +} From 2683047c0a6b3e67de1696a67b37b844c20fabe1 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 2 Jul 2026 21:10:39 +0000 Subject: [PATCH 04/17] feat(usb): native isochronous UAC driver and UsbAudioSession wrapper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The transfer plane of the USB DAC driver, in C++ on libusb: - UacDevice: wraps the UsbDeviceConnection fd (libusb_wrap_sys_device + NO_DEVICE_DISCOVERY — no root), auto-detaches the kernel audio driver with bounded retries on BUSY, claims AC+AS interfaces, programs alt settings, sample rate (UAC2 clock SET CUR / UAC1 endpoint SET_CUR) and feature-unit volume/mute in native 1/256 dB units - IsoStream: 8 pre-allocated iso OUT transfers × 8 packets kept in flight from a lock-free SPSC ring buffer; packet sizing by Q16.16 fractional accumulator (nominal rate, corrected by the explicit feedback endpoint on async DACs — Q10.14 full-speed / Q16.16 high-speed, sanity-windowed ±25%); underruns zero-fill and count xruns; pause keeps silent packets flowing so the DAC clock stays locked; stop cancels and drops to alt 0 to release iso bandwidth. Separate played vs consumed frame counters give the sink an accurate position and a clean flush rebase point - JNI bridge with a mutex-guarded session object; PCM crosses via direct ByteBuffers, non-blocking with backpressure (0 = ring full) - UsbAudioSession (Kotlin): owns handle + connection, typed configure/ write/position/volume API, safe after close so a racing detach can't crash the playback thread Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FZMSPh3TgbRAMqgW4q1p6V --- usbaudio/src/main/cpp/CMakeLists.txt | 4 +- usbaudio/src/main/cpp/iso_stream.cpp | 314 ++++++++++++++++++ usbaudio/src/main/cpp/iso_stream.h | 122 +++++++ usbaudio/src/main/cpp/jni_bridge.cpp | 253 +++++++++++++- usbaudio/src/main/cpp/log.h | 8 + usbaudio/src/main/cpp/ring_buffer.h | 74 +++++ usbaudio/src/main/cpp/uac_device.cpp | 226 +++++++++++++ usbaudio/src/main/cpp/uac_device.h | 59 ++++ .../pixelplay/usbaudio/UsbAudioNative.kt | 95 +++++- .../pixelplay/usbaudio/UsbAudioSession.kt | 201 +++++++++++ 10 files changed, 1352 insertions(+), 4 deletions(-) create mode 100644 usbaudio/src/main/cpp/iso_stream.cpp create mode 100644 usbaudio/src/main/cpp/iso_stream.h create mode 100644 usbaudio/src/main/cpp/log.h create mode 100644 usbaudio/src/main/cpp/ring_buffer.h create mode 100644 usbaudio/src/main/cpp/uac_device.cpp create mode 100644 usbaudio/src/main/cpp/uac_device.h create mode 100644 usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/UsbAudioSession.kt diff --git a/usbaudio/src/main/cpp/CMakeLists.txt b/usbaudio/src/main/cpp/CMakeLists.txt index 8f71d4b5d..082a15013 100644 --- a/usbaudio/src/main/cpp/CMakeLists.txt +++ b/usbaudio/src/main/cpp/CMakeLists.txt @@ -31,9 +31,11 @@ target_include_directories(usb-1.0 target_compile_options(usb-1.0 PRIVATE -fvisibility=hidden -pthread) target_link_libraries(usb-1.0 PRIVATE log) -# --- JNI bridge ---------------------------------------------------------------- +# --- JNI bridge + driver -------------------------------------------------------- add_library(usbaudio_jni SHARED jni_bridge.cpp + uac_device.cpp + iso_stream.cpp ) target_link_libraries(usbaudio_jni PRIVATE usb-1.0 log) diff --git a/usbaudio/src/main/cpp/iso_stream.cpp b/usbaudio/src/main/cpp/iso_stream.cpp new file mode 100644 index 000000000..6bdc41e26 --- /dev/null +++ b/usbaudio/src/main/cpp/iso_stream.cpp @@ -0,0 +1,314 @@ +#include "iso_stream.h" + +#include +#include + +#include "log.h" +#include "uac_device.h" + +namespace { +constexpr int kTransfers = 8; +constexpr int kPacketsPerTransferTarget = 8; +constexpr unsigned kEventTimeoutUs = 100 * 1000; +} // namespace + +IsoStream::IsoStream(UacDevice& device, Config config) + : device_(device), config_(config) {} + +IsoStream::~IsoStream() { + stop(); +} + +bool IsoStream::start() { + if (running_.load()) return true; + + frameBytes_ = config_.channels * config_.subslotBytes; + if (frameBytes_ <= 0 || config_.rateHz == 0 || config_.maxPacketSize <= 0) { + device_.setLastError("IsoStream: invalid configuration"); + return false; + } + + // Packets per second on the wire: 1000 frames (full speed) or 8000 µframes (high + // speed), stretched by the endpoint's service interval (2^(bInterval-1)). + const int base = config_.highSpeed ? 8000 : 1000; + const int intervalShift = config_.intervalCode > 0 ? config_.intervalCode - 1 : 0; + packetsPerSecond_ = base >> (intervalShift > 4 ? 4 : intervalShift); + if (packetsPerSecond_ <= 0) packetsPerSecond_ = 1000; + + nominalRateQ16_ = (static_cast(config_.rateHz) << 16) / packetsPerSecond_; + maxFramesPerPacket_ = static_cast(config_.maxPacketSize / frameBytes_); + const uint32_t nominalFrames = static_cast(nominalRateQ16_ >> 16) + 1; + if (maxFramesPerPacket_ < nominalFrames) { + device_.setLastError("IsoStream: wMaxPacketSize too small for rate"); + UA_LOGE("maxPacket=%d < needed %u frames × %d bytes", config_.maxPacketSize, + nominalFrames, frameBytes_); + return false; + } + + packetsPerTransfer_ = kPacketsPerTransferTarget; + const size_t ringBytes = + static_cast(config_.rateHz) * frameBytes_ * config_.ringBufferMs / 1000; + ring_ = std::make_unique(ringBytes > 4096 ? ringBytes : 4096); + + rateAccumulatorQ16_ = 0; + stopping_.store(false); + dead_.store(false); + running_.store(true); + + // Allocate the pipeline before spawning the event thread. + const size_t packetBytes = static_cast(maxFramesPerPacket_) * frameBytes_; + transfers_.clear(); + for (int i = 0; i < kTransfers; ++i) { + auto context = std::make_unique(); + context->stream = this; + context->buffer.resize(packetBytes * packetsPerTransfer_); + context->transfer = libusb_alloc_transfer(packetsPerTransfer_); + if (context->transfer == nullptr) { + device_.setLastError("IsoStream: libusb_alloc_transfer failed"); + running_.store(false); + return false; + } + transfers_.push_back(std::move(context)); + } + + if (config_.feedbackEndpointAddress != 0) { + feedbackBuffer_.resize(4); + feedbackTransfer_ = libusb_alloc_transfer(1); + if (feedbackTransfer_ == nullptr) { + device_.setLastError("IsoStream: feedback transfer alloc failed"); + running_.store(false); + return false; + } + } + + for (auto& context : transfers_) { + fillAndSubmit(*context); + if (dead_.load()) { + running_.store(false); + return false; + } + } + if (feedbackTransfer_ != nullptr && !submitFeedback()) { + // Feedback is an optimization; fall back to nominal-rate pacing. + UA_LOGW("Feedback endpoint submit failed; using nominal rate"); + } + + eventThread_ = std::thread([this] { eventLoop(); }); + UA_LOGI("IsoStream started: %u Hz × %dch × %dB, %d pkt/s, maxFrames/pkt=%u", + config_.rateHz, config_.channels, config_.subslotBytes, packetsPerSecond_, + maxFramesPerPacket_); + return true; +} + +void IsoStream::stop() { + if (!running_.load()) return; + stopping_.store(true); + + for (auto& context : transfers_) { + if (context->inFlight) { + libusb_cancel_transfer(context->transfer); + } + } + if (feedbackTransfer_ != nullptr && feedbackInFlight_) { + libusb_cancel_transfer(feedbackTransfer_); + } + + if (eventThread_.joinable()) { + eventThread_.join(); + } + + for (auto& context : transfers_) { + libusb_free_transfer(context->transfer); + context->transfer = nullptr; + } + transfers_.clear(); + if (feedbackTransfer_ != nullptr) { + libusb_free_transfer(feedbackTransfer_); + feedbackTransfer_ = nullptr; + } + running_.store(false); + UA_LOGI("IsoStream stopped (played=%" PRIu64 " frames, xruns=%d)", + playedFrames_.load(), xruns_.load()); +} + +void IsoStream::flush() { + if (ring_) ring_->clear(); + // In-flight audio will still play out (a few ms); consumedFrames() already includes it, + // which is exactly what the sink snapshots as its flush base. +} + +int IsoStream::write(const uint8_t* data, size_t size) { + if (dead_.load(std::memory_order_acquire)) return -1; + if (!running_.load(std::memory_order_acquire) || !ring_) return -1; + return static_cast(ring_->write(data, size)); +} + +uint64_t IsoStream::bufferedFrames() const { + const uint64_t ringFrames = ring_ ? ring_->availableToRead() / frameBytes_ : 0; + return ringFrames + inFlightFrames_.load(std::memory_order_acquire); +} + +uint32_t IsoStream::nextPacketFrames() { + const uint64_t feedback = feedbackRateQ16_.load(std::memory_order_acquire); + const uint64_t rate = feedback != 0 ? feedback : nominalRateQ16_; + rateAccumulatorQ16_ += rate; + uint32_t frames = static_cast(rateAccumulatorQ16_ >> 16); + rateAccumulatorQ16_ &= 0xFFFF; + if (frames > maxFramesPerPacket_) { + // Bogus feedback value; clamp and drop the excess debt. + frames = maxFramesPerPacket_; + rateAccumulatorQ16_ = 0; + } + return frames; +} + +void IsoStream::fillAndSubmit(TransferContext& context) { + libusb_transfer* transfer = context.transfer; + uint8_t* cursor = context.buffer.data(); + uint32_t dataFrames = 0; + int totalBytes = 0; + + const bool silent = paused_.load(std::memory_order_acquire); + + for (int p = 0; p < packetsPerTransfer_; ++p) { + const uint32_t frames = nextPacketFrames(); + const size_t bytes = static_cast(frames) * frameBytes_; + size_t got = 0; + if (!silent && bytes > 0) { + got = ring_->read(cursor, bytes); + if (got < bytes) { + // Underrun: pad with silence. Only count once per packet while playing. + std::memset(cursor + got, 0, bytes - got); + xruns_.fetch_add(1, std::memory_order_relaxed); + } + } else if (bytes > 0) { + std::memset(cursor, 0, bytes); + } + dataFrames += static_cast(got / frameBytes_); + transfer->iso_packet_desc[p].length = static_cast(bytes); + cursor += bytes; + totalBytes += static_cast(bytes); + } + + context.dataFrames = dataFrames; + if (dataFrames > 0) { + consumedFrames_.fetch_add(dataFrames, std::memory_order_release); + inFlightFrames_.fetch_add(dataFrames, std::memory_order_release); + } + + libusb_fill_iso_transfer(transfer, device_.handle(), config_.endpointAddress, + context.buffer.data(), totalBytes, packetsPerTransfer_, + &IsoStream::onTransferComplete, &context, 0); + + const int rc = libusb_submit_transfer(transfer); + if (rc != LIBUSB_SUCCESS) { + context.inFlight = false; + if (dataFrames > 0) { + inFlightFrames_.fetch_sub(dataFrames, std::memory_order_release); + } + device_.setLastError(std::string("iso submit: ") + libusb_error_name(rc)); + markDead("submit failed"); + return; + } + context.inFlight = true; + pendingTransfers_.fetch_add(1, std::memory_order_release); +} + +void IsoStream::onTransferComplete(libusb_transfer* transfer) { + auto* context = static_cast(transfer->user_data); + IsoStream* self = context->stream; + + context->inFlight = false; + self->pendingTransfers_.fetch_sub(1, std::memory_order_release); + + const uint32_t dataFrames = context->dataFrames; + if (dataFrames > 0) { + self->inFlightFrames_.fetch_sub(dataFrames, std::memory_order_release); + } + + switch (transfer->status) { + case LIBUSB_TRANSFER_COMPLETED: + if (dataFrames > 0) { + self->playedFrames_.fetch_add(dataFrames, std::memory_order_release); + } + break; + case LIBUSB_TRANSFER_CANCELLED: + return; // shutting down; do not resubmit + case LIBUSB_TRANSFER_NO_DEVICE: + self->markDead("device gone"); + return; + default: + // Isolated iso errors happen (bus glitches); resubmit unless we are stopping. + UA_LOGW("iso transfer status=%d", transfer->status); + break; + } + + if (!self->stopping_.load(std::memory_order_acquire) && !self->dead_.load()) { + self->fillAndSubmit(*context); + } +} + +bool IsoStream::submitFeedback() { + libusb_fill_iso_transfer(feedbackTransfer_, device_.handle(), + config_.feedbackEndpointAddress, feedbackBuffer_.data(), + static_cast(feedbackBuffer_.size()), 1, + &IsoStream::onFeedbackComplete, this, 0); + feedbackTransfer_->iso_packet_desc[0].length = static_cast(feedbackBuffer_.size()); + const int rc = libusb_submit_transfer(feedbackTransfer_); + feedbackInFlight_ = rc == LIBUSB_SUCCESS; + return feedbackInFlight_; +} + +void IsoStream::onFeedbackComplete(libusb_transfer* transfer) { + auto* self = static_cast(transfer->user_data); + self->feedbackInFlight_ = false; + + if (transfer->status == LIBUSB_TRANSFER_CANCELLED) return; + if (transfer->status == LIBUSB_TRANSFER_NO_DEVICE) { + self->markDead("device gone (feedback)"); + return; + } + + if (transfer->status == LIBUSB_TRANSFER_COMPLETED) { + const int length = transfer->iso_packet_desc[0].actual_length; + const uint8_t* data = self->feedbackBuffer_.data(); + uint64_t rateQ16 = 0; + if (length >= 4) { + // High-speed: Q16.16 frames per µframe. + const uint32_t raw = data[0] | (data[1] << 8) | (data[2] << 16) + | (static_cast(data[3]) << 24); + rateQ16 = raw; + } else if (length == 3) { + // Full-speed: Q10.14 frames per frame → shift to Q16.16. + const uint32_t raw = data[0] | (data[1] << 8) | (data[2] << 16); + rateQ16 = static_cast(raw) << 2; + } + if (rateQ16 != 0) { + // Sanity window: ±25% of nominal, otherwise ignore the sample. + const uint64_t nominal = self->nominalRateQ16_; + if (rateQ16 > nominal - nominal / 4 && rateQ16 < nominal + nominal / 4) { + self->feedbackRateQ16_.store(rateQ16, std::memory_order_release); + } + } + } + + if (!self->stopping_.load(std::memory_order_acquire) && !self->dead_.load()) { + self->submitFeedback(); + } +} + +void IsoStream::markDead(const char* why) { + if (!dead_.exchange(true)) { + UA_LOGE("IsoStream dead: %s", why); + } +} + +void IsoStream::eventLoop() { + timeval timeout{0, kEventTimeoutUs}; + while (true) { + const bool stopping = stopping_.load(std::memory_order_acquire); + const int pending = pendingTransfers_.load(std::memory_order_acquire); + if (stopping && pending == 0 && !feedbackInFlight_) break; + libusb_handle_events_timeout(device_.context(), &timeout); + } +} diff --git a/usbaudio/src/main/cpp/iso_stream.h b/usbaudio/src/main/cpp/iso_stream.h new file mode 100644 index 000000000..ac45d45b6 --- /dev/null +++ b/usbaudio/src/main/cpp/iso_stream.h @@ -0,0 +1,122 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "libusb.h" +#include "ring_buffer.h" + +class UacDevice; + +/** + * Isochronous OUT streaming engine. + * + * A fixed set of transfers (each carrying several iso packets) is kept in flight; every + * completion callback refills the transfer from the ring buffer and resubmits it, so the + * pipeline never drains as long as the producer keeps up. Packet sizes follow a fractional + * accumulator seeded with the nominal rate and, for asynchronous endpoints with an explicit + * feedback endpoint, continuously corrected by the DAC's feedback value. + * + * Underruns are filled with silence (and counted) rather than stalling the bus; pausing + * keeps the transfers running silence-only so the DAC's clock stays locked (no resume pop). + */ +class IsoStream { +public: + struct Config { + uint8_t endpointAddress = 0; + uint8_t feedbackEndpointAddress = 0; // 0 = none + uint32_t rateHz = 0; + int channels = 0; + int subslotBytes = 0; + int maxPacketSize = 0; + int intervalCode = 1; // endpoint bInterval + bool highSpeed = false; + int ringBufferMs = 250; + }; + + IsoStream(UacDevice& device, Config config); + ~IsoStream(); + + IsoStream(const IsoStream&) = delete; + IsoStream& operator=(const IsoStream&) = delete; + + /** Allocates and submits the transfer pipeline, starts the event thread. */ + bool start(); + /** Cancels everything and joins the event thread. Idempotent. */ + void stop(); + + void pause() { paused_.store(true, std::memory_order_release); } + void resume() { paused_.store(false, std::memory_order_release); } + bool paused() const { return paused_.load(std::memory_order_acquire); } + + /** Drops all buffered (not yet consumed) audio. */ + void flush(); + + /** Producer side; returns bytes accepted (0 = ring full), -1 when the stream is dead. */ + int write(const uint8_t* data, size_t size); + + /** Frames of real audio handed to *completed* transfers (drives the playback position). */ + uint64_t playedFrames() const { return playedFrames_.load(std::memory_order_acquire); } + /** Frames of real audio consumed from the ring (played + in flight). */ + uint64_t consumedFrames() const { return consumedFrames_.load(std::memory_order_acquire); } + /** Frames still queued: ring + in flight. */ + uint64_t bufferedFrames() const; + int xrunCount() const { return xruns_.load(std::memory_order_acquire); } + /** False once the device vanished or the pipeline hit a fatal error. */ + bool alive() const { return !dead_.load(std::memory_order_acquire); } + +private: + struct TransferContext { + IsoStream* stream = nullptr; + libusb_transfer* transfer = nullptr; + std::vector buffer; + uint32_t dataFrames = 0; // real (non-silence) frames in the submitted transfer + bool inFlight = false; + }; + + static void onTransferComplete(libusb_transfer* transfer); + static void onFeedbackComplete(libusb_transfer* transfer); + + void fillAndSubmit(TransferContext& context); + bool submitFeedback(); + void markDead(const char* why); + void eventLoop(); + + /** Next packet size in frames, from the fractional accumulator. */ + uint32_t nextPacketFrames(); + + UacDevice& device_; + const Config config_; + + int frameBytes_ = 0; + int packetsPerSecond_ = 0; + uint32_t maxFramesPerPacket_ = 0; + int packetsPerTransfer_ = 0; + + /** Q16.16 frames-per-packet, nominal and (when feedback is live) corrected. */ + uint64_t nominalRateQ16_ = 0; + std::atomic feedbackRateQ16_{0}; // 0 = no feedback yet + uint64_t rateAccumulatorQ16_ = 0; // event-thread only + + std::unique_ptr ring_; + std::vector> transfers_; + + libusb_transfer* feedbackTransfer_ = nullptr; + std::vector feedbackBuffer_; + bool feedbackInFlight_ = false; + + std::thread eventThread_; + std::atomic running_{false}; + std::atomic stopping_{false}; + std::atomic paused_{false}; + std::atomic dead_{false}; + + std::atomic playedFrames_{0}; + std::atomic consumedFrames_{0}; + std::atomic inFlightFrames_{0}; + std::atomic xruns_{0}; + std::atomic pendingTransfers_{0}; +}; diff --git a/usbaudio/src/main/cpp/jni_bridge.cpp b/usbaudio/src/main/cpp/jni_bridge.cpp index a65b9c9d2..524b81b16 100644 --- a/usbaudio/src/main/cpp/jni_bridge.cpp +++ b/usbaudio/src/main/cpp/jni_bridge.cpp @@ -1,13 +1,262 @@ #include #include +#include +#include +#include "iso_stream.h" #include "libusb.h" +#include "log.h" +#include "uac_device.h" -extern "C" JNIEXPORT jstring JNICALL -Java_com_theveloper_pixelplay_usbaudio_UsbAudioNative_nativeGetVersion(JNIEnv* env, jobject /*thiz*/) { +namespace { + +/** Everything one Kotlin UsbAudioSession owns natively. */ +struct NativeSession { + std::unique_ptr device; + std::shared_ptr stream; + std::mutex mutex; +}; + +NativeSession* fromHandle(jlong handle) { + return reinterpret_cast(handle); +} + +std::shared_ptr streamOf(jlong handle) { + NativeSession* session = fromHandle(handle); + if (session == nullptr) return nullptr; + std::lock_guard lock(session->mutex); + return session->stream; +} + +} // namespace + +extern "C" { + +JNIEXPORT jstring JNICALL +Java_com_theveloper_pixelplay_usbaudio_UsbAudioNative_nativeGetVersion(JNIEnv* env, jobject) { const libusb_version* v = libusb_get_version(); char buf[64]; std::snprintf(buf, sizeof(buf), "libusb %u.%u.%u", v->major, v->minor, v->micro); return env->NewStringUTF(buf); } + +JNIEXPORT jlong JNICALL +Java_com_theveloper_pixelplay_usbaudio_UsbAudioNative_nativeCreate(JNIEnv*, jobject, jint fd) { + std::string error; + auto device = UacDevice::create(fd, &error); + if (!device) { + UA_LOGE("nativeCreate failed: %s", error.c_str()); + return 0; + } + auto* session = new NativeSession(); + session->device = std::move(device); + return reinterpret_cast(session); +} + +JNIEXPORT void JNICALL +Java_com_theveloper_pixelplay_usbaudio_UsbAudioNative_nativeDestroy(JNIEnv*, jobject, jlong handle) { + NativeSession* session = fromHandle(handle); + if (session == nullptr) return; + { + std::lock_guard lock(session->mutex); + if (session->stream) session->stream->stop(); + session->stream.reset(); + session->device.reset(); + } + delete session; +} + +JNIEXPORT jint JNICALL +Java_com_theveloper_pixelplay_usbaudio_UsbAudioNative_nativeClaim( + JNIEnv*, jobject, jlong handle, jint acInterface, jint asInterface) { + NativeSession* session = fromHandle(handle); + if (session == nullptr) return -1; + std::lock_guard lock(session->mutex); + return session->device->claimInterfaces(acInterface, asInterface) ? 0 : -1; +} + +JNIEXPORT jint JNICALL +Java_com_theveloper_pixelplay_usbaudio_UsbAudioNative_nativeConfigureStream( + JNIEnv*, jobject, jlong handle, jint asInterface, jint altSetting, jint endpointAddress, + jint feedbackEndpointAddress, jint rateHz, jint channels, jint subslotBytes, + jint intervalCode, jint maxPacketSize, jint ringBufferMs) { + NativeSession* session = fromHandle(handle); + if (session == nullptr) return -1; + std::lock_guard lock(session->mutex); + + if (session->stream) { + session->stream->stop(); + session->stream.reset(); + } + if (!session->device->setAltSetting(asInterface, altSetting)) { + return -1; + } + + IsoStream::Config config; + config.endpointAddress = static_cast(endpointAddress); + config.feedbackEndpointAddress = static_cast(feedbackEndpointAddress); + config.rateHz = static_cast(rateHz); + config.channels = channels; + config.subslotBytes = subslotBytes; + config.intervalCode = intervalCode; + config.maxPacketSize = maxPacketSize; + config.highSpeed = session->device->isHighSpeed(); + config.ringBufferMs = ringBufferMs; + + session->stream = std::make_shared(*session->device, config); + return 0; +} + +JNIEXPORT jint JNICALL +Java_com_theveloper_pixelplay_usbaudio_UsbAudioNative_nativeSetSampleRate( + JNIEnv*, jobject, jlong handle, jint uacVersion, jint clockId, jint acInterface, + jint endpointAddress, jint rateHz) { + NativeSession* session = fromHandle(handle); + if (session == nullptr) return -1; + std::lock_guard lock(session->mutex); + const bool ok = uacVersion == 2 + ? session->device->setSampleRateUac2(clockId, acInterface, static_cast(rateHz)) + : session->device->setSampleRateUac1(endpointAddress, static_cast(rateHz)); + return ok ? 0 : -1; +} + +JNIEXPORT jint JNICALL +Java_com_theveloper_pixelplay_usbaudio_UsbAudioNative_nativeStart(JNIEnv*, jobject, jlong handle) { + auto stream = streamOf(handle); + if (!stream) return -1; + return stream->start() ? 0 : -1; +} + +JNIEXPORT jint JNICALL +Java_com_theveloper_pixelplay_usbaudio_UsbAudioNative_nativePause(JNIEnv*, jobject, jlong handle) { + auto stream = streamOf(handle); + if (!stream) return -1; + stream->pause(); + return 0; +} + +JNIEXPORT jint JNICALL +Java_com_theveloper_pixelplay_usbaudio_UsbAudioNative_nativeResume(JNIEnv*, jobject, jlong handle) { + auto stream = streamOf(handle); + if (!stream) return -1; + stream->resume(); + return 0; +} + +JNIEXPORT jint JNICALL +Java_com_theveloper_pixelplay_usbaudio_UsbAudioNative_nativeFlush(JNIEnv*, jobject, jlong handle) { + auto stream = streamOf(handle); + if (!stream) return -1; + stream->flush(); + return 0; +} + +JNIEXPORT jint JNICALL +Java_com_theveloper_pixelplay_usbaudio_UsbAudioNative_nativeStop( + JNIEnv*, jobject, jlong handle, jint asInterface) { + NativeSession* session = fromHandle(handle); + if (session == nullptr) return -1; + std::lock_guard lock(session->mutex); + if (session->stream) { + session->stream->stop(); + session->stream.reset(); + } + // Alt setting 0 releases the reserved isochronous bandwidth. + session->device->setAltSetting(asInterface, 0); + return 0; +} + +JNIEXPORT jint JNICALL +Java_com_theveloper_pixelplay_usbaudio_UsbAudioNative_nativeWrite( + JNIEnv* env, jobject, jlong handle, jobject buffer, jint offset, jint size) { + auto stream = streamOf(handle); + if (!stream) return -1; + auto* base = static_cast(env->GetDirectBufferAddress(buffer)); + if (base == nullptr) return -2; // not a direct buffer + const jlong capacity = env->GetDirectBufferCapacity(buffer); + if (offset < 0 || size < 0 || offset + size > capacity) return -3; + return stream->write(base + offset, static_cast(size)); +} + +JNIEXPORT jlong JNICALL +Java_com_theveloper_pixelplay_usbaudio_UsbAudioNative_nativeGetPlayedFrames( + JNIEnv*, jobject, jlong handle) { + auto stream = streamOf(handle); + return stream ? static_cast(stream->playedFrames()) : 0; +} + +JNIEXPORT jlong JNICALL +Java_com_theveloper_pixelplay_usbaudio_UsbAudioNative_nativeGetConsumedFrames( + JNIEnv*, jobject, jlong handle) { + auto stream = streamOf(handle); + return stream ? static_cast(stream->consumedFrames()) : 0; +} + +JNIEXPORT jlong JNICALL +Java_com_theveloper_pixelplay_usbaudio_UsbAudioNative_nativeGetBufferedFrames( + JNIEnv*, jobject, jlong handle) { + auto stream = streamOf(handle); + return stream ? static_cast(stream->bufferedFrames()) : 0; +} + +JNIEXPORT jint JNICALL +Java_com_theveloper_pixelplay_usbaudio_UsbAudioNative_nativeGetXrunCount( + JNIEnv*, jobject, jlong handle) { + auto stream = streamOf(handle); + return stream ? stream->xrunCount() : 0; +} + +JNIEXPORT jboolean JNICALL +Java_com_theveloper_pixelplay_usbaudio_UsbAudioNative_nativeIsAlive( + JNIEnv*, jobject, jlong handle) { + auto stream = streamOf(handle); + return stream && stream->alive() ? JNI_TRUE : JNI_FALSE; +} + +JNIEXPORT jintArray JNICALL +Java_com_theveloper_pixelplay_usbaudio_UsbAudioNative_nativeGetVolumeRangeDb256( + JNIEnv* env, jobject, jlong handle, jint uacVersion, jint unitId, jint acInterface) { + NativeSession* session = fromHandle(handle); + if (session == nullptr) return nullptr; + std::lock_guard lock(session->mutex); + int32_t range[3] = {0, 0, 0}; + if (!session->device->getVolumeRangeDb256(uacVersion, unitId, acInterface, range)) { + return nullptr; + } + jintArray out = env->NewIntArray(3); + if (out == nullptr) return nullptr; + env->SetIntArrayRegion(out, 0, 3, reinterpret_cast(range)); + return out; +} + +JNIEXPORT jint JNICALL +Java_com_theveloper_pixelplay_usbaudio_UsbAudioNative_nativeSetVolumeDb256( + JNIEnv*, jobject, jlong handle, jint uacVersion, jint unitId, jint acInterface, jint valueDb256) { + NativeSession* session = fromHandle(handle); + if (session == nullptr) return -1; + std::lock_guard lock(session->mutex); + return session->device->setVolumeDb256(uacVersion, unitId, acInterface, valueDb256) ? 0 : -1; +} + +JNIEXPORT jint JNICALL +Java_com_theveloper_pixelplay_usbaudio_UsbAudioNative_nativeSetMute( + JNIEnv*, jobject, jlong handle, jint uacVersion, jint unitId, jint acInterface, jboolean mute) { + NativeSession* session = fromHandle(handle); + if (session == nullptr) return -1; + std::lock_guard lock(session->mutex); + return session->device->setMute(uacVersion, unitId, acInterface, mute == JNI_TRUE) ? 0 : -1; +} + +JNIEXPORT jstring JNICALL +Java_com_theveloper_pixelplay_usbaudio_UsbAudioNative_nativeGetLastError( + JNIEnv* env, jobject, jlong handle) { + NativeSession* session = fromHandle(handle); + if (session == nullptr) return nullptr; + std::lock_guard lock(session->mutex); + const std::string error = session->device ? session->device->lastError() : ""; + if (error.empty()) return nullptr; + return env->NewStringUTF(error.c_str()); +} + +} // extern "C" diff --git a/usbaudio/src/main/cpp/log.h b/usbaudio/src/main/cpp/log.h new file mode 100644 index 000000000..8ed0fd73c --- /dev/null +++ b/usbaudio/src/main/cpp/log.h @@ -0,0 +1,8 @@ +#pragma once + +#include + +#define UA_LOG_TAG "UsbAudioNative" +#define UA_LOGI(...) __android_log_print(ANDROID_LOG_INFO, UA_LOG_TAG, __VA_ARGS__) +#define UA_LOGW(...) __android_log_print(ANDROID_LOG_WARN, UA_LOG_TAG, __VA_ARGS__) +#define UA_LOGE(...) __android_log_print(ANDROID_LOG_ERROR, UA_LOG_TAG, __VA_ARGS__) diff --git a/usbaudio/src/main/cpp/ring_buffer.h b/usbaudio/src/main/cpp/ring_buffer.h new file mode 100644 index 000000000..540968bb9 --- /dev/null +++ b/usbaudio/src/main/cpp/ring_buffer.h @@ -0,0 +1,74 @@ +#pragma once + +#include +#include +#include +#include + +/** + * Lock-free single-producer/single-consumer byte ring. + * Producer: the JNI write path (ExoPlayer's playback thread). + * Consumer: the libusb event thread filling isochronous packets. + * + * Positions are monotonically increasing byte counts; the index into the + * backing store is position % capacity. + */ +class RingBuffer { +public: + explicit RingBuffer(size_t capacity) : buffer_(capacity) {} + + size_t capacity() const { return buffer_.size(); } + + size_t availableToRead() const { + return static_cast( + writePos_.load(std::memory_order_acquire) - readPos_.load(std::memory_order_acquire)); + } + + size_t availableToWrite() const { return capacity() - availableToRead(); } + + /** Copies up to `size` bytes in; returns the number accepted (0 when full). */ + size_t write(const uint8_t* data, size_t size) { + const uint64_t writePos = writePos_.load(std::memory_order_relaxed); + const uint64_t readPos = readPos_.load(std::memory_order_acquire); + const size_t space = capacity() - static_cast(writePos - readPos); + const size_t toWrite = size < space ? size : space; + if (toWrite == 0) return 0; + + const size_t index = static_cast(writePos % capacity()); + const size_t first = std::min(toWrite, capacity() - index); + std::memcpy(buffer_.data() + index, data, first); + if (toWrite > first) { + std::memcpy(buffer_.data(), data + first, toWrite - first); + } + writePos_.store(writePos + toWrite, std::memory_order_release); + return toWrite; + } + + /** Copies up to `size` bytes out; returns the number read. */ + size_t read(uint8_t* out, size_t size) { + const uint64_t readPos = readPos_.load(std::memory_order_relaxed); + const uint64_t writePos = writePos_.load(std::memory_order_acquire); + const size_t available = static_cast(writePos - readPos); + const size_t toRead = size < available ? size : available; + if (toRead == 0) return 0; + + const size_t index = static_cast(readPos % capacity()); + const size_t first = std::min(toRead, capacity() - index); + std::memcpy(out, buffer_.data() + index, first); + if (toRead > first) { + std::memcpy(out + first, buffer_.data(), toRead - first); + } + readPos_.store(readPos + toRead, std::memory_order_release); + return toRead; + } + + /** Consumer-side discard of everything currently buffered. */ + void clear() { + readPos_.store(writePos_.load(std::memory_order_acquire), std::memory_order_release); + } + +private: + std::vector buffer_; + std::atomic writePos_{0}; + std::atomic readPos_{0}; +}; diff --git a/usbaudio/src/main/cpp/uac_device.cpp b/usbaudio/src/main/cpp/uac_device.cpp new file mode 100644 index 000000000..2eb9bca7c --- /dev/null +++ b/usbaudio/src/main/cpp/uac_device.cpp @@ -0,0 +1,226 @@ +#include "uac_device.h" + +#include + +#include "log.h" + +namespace { + +// bmRequestType values +constexpr uint8_t kClassInterfaceOut = 0x21; // host→device | class | interface +constexpr uint8_t kClassInterfaceIn = 0xA1; // device→host | class | interface +constexpr uint8_t kClassEndpointOut = 0x22; // host→device | class | endpoint + +// Shared request codes +constexpr uint8_t kReqCur = 0x01; +constexpr uint8_t kReqRange = 0x02; // UAC2 only +// UAC1 GET_MIN/GET_MAX/GET_RES +constexpr uint8_t kReqGetMin = 0x82; +constexpr uint8_t kReqGetMax = 0x83; +constexpr uint8_t kReqGetRes = 0x84; + +// Control selectors +constexpr uint8_t kCsSamFreqControl = 0x01; // clock source (UAC2) / endpoint (UAC1) +constexpr uint8_t kFuMuteControl = 0x01; +constexpr uint8_t kFuVolumeControl = 0x02; + +constexpr unsigned kControlTimeoutMs = 1000; +constexpr int kClaimRetries = 3; + +} // namespace + +std::unique_ptr UacDevice::create(int fd, std::string* errorOut) { + std::unique_ptr device(new UacDevice()); + + // The Android USB host API hands us an already-open usbfs fd; libusb must not try to + // enumerate /dev/bus/usb itself (no permission without root). + libusb_set_option(nullptr, LIBUSB_OPTION_NO_DEVICE_DISCOVERY); + + int rc = libusb_init(&device->ctx_); + if (rc != LIBUSB_SUCCESS) { + if (errorOut) *errorOut = std::string("libusb_init: ") + libusb_error_name(rc); + return nullptr; + } + + rc = libusb_wrap_sys_device(device->ctx_, static_cast(fd), &device->handle_); + if (rc != LIBUSB_SUCCESS || device->handle_ == nullptr) { + if (errorOut) *errorOut = std::string("libusb_wrap_sys_device: ") + libusb_error_name(rc); + return nullptr; + } + + // Have libusb move the kernel's snd-usb-audio driver out of the way on claim and + // hand the interface back on release. + libusb_set_auto_detach_kernel_driver(device->handle_, 1); + + UA_LOGI("UacDevice created (fd=%d, highSpeed=%d)", fd, device->isHighSpeed() ? 1 : 0); + return device; +} + +UacDevice::~UacDevice() { + if (handle_ != nullptr) { + if (claimedAs_ >= 0) libusb_release_interface(handle_, claimedAs_); + if (claimedAc_ >= 0) libusb_release_interface(handle_, claimedAc_); + libusb_close(handle_); + } + if (ctx_ != nullptr) { + libusb_exit(ctx_); + } +} + +bool UacDevice::claimInterfaces(int acInterface, int asInterface) { + struct Claim { + int number; + int* slot; + } claims[] = {{acInterface, &claimedAc_}, {asInterface, &claimedAs_}}; + + for (const auto& claim : claims) { + if (*claim.slot == claim.number) continue; // already claimed + int rc = LIBUSB_ERROR_OTHER; + for (int attempt = 0; attempt < kClaimRetries; ++attempt) { + rc = libusb_claim_interface(handle_, claim.number); + if (rc == LIBUSB_SUCCESS) break; + if (rc != LIBUSB_ERROR_BUSY) break; + // The audio HAL may still hold the interface right after permission grant. + libusb_detach_kernel_driver(handle_, claim.number); + } + if (rc != LIBUSB_SUCCESS) { + setLastError(std::string("claim interface ") + std::to_string(claim.number) + ": " + + libusb_error_name(rc)); + UA_LOGE("%s", lastError().c_str()); + return false; + } + *claim.slot = claim.number; + } + UA_LOGI("Claimed AC=%d AS=%d", acInterface, asInterface); + return true; +} + +bool UacDevice::setAltSetting(int asInterface, int altSetting) { + int rc = libusb_set_interface_alt_setting(handle_, asInterface, altSetting); + if (rc != LIBUSB_SUCCESS) { + setLastError(std::string("set_interface_alt_setting(") + std::to_string(asInterface) + + ", " + std::to_string(altSetting) + "): " + libusb_error_name(rc)); + UA_LOGE("%s", lastError().c_str()); + return false; + } + return true; +} + +bool UacDevice::setSampleRateUac2(int clockId, int acInterface, uint32_t rateHz) { + const uint8_t data[4] = { + static_cast(rateHz & 0xFF), + static_cast((rateHz >> 8) & 0xFF), + static_cast((rateHz >> 16) & 0xFF), + static_cast((rateHz >> 24) & 0xFF), + }; + const uint16_t value = static_cast(kCsSamFreqControl << 8); + const uint16_t index = static_cast((clockId << 8) | (acInterface & 0xFF)); + if (!controlOut(kClassInterfaceOut, kReqCur, value, index, data, sizeof(data))) { + return false; + } + UA_LOGI("UAC2 clock %d set to %" PRIu32 " Hz", clockId, rateHz); + return true; +} + +bool UacDevice::setSampleRateUac1(int endpointAddress, uint32_t rateHz) { + const uint8_t data[3] = { + static_cast(rateHz & 0xFF), + static_cast((rateHz >> 8) & 0xFF), + static_cast((rateHz >> 16) & 0xFF), + }; + const uint16_t value = static_cast(kCsSamFreqControl << 8); + const uint16_t index = static_cast(endpointAddress & 0xFF); + if (!controlOut(kClassEndpointOut, kReqCur, value, index, data, sizeof(data))) { + return false; + } + UA_LOGI("UAC1 endpoint 0x%02x set to %" PRIu32 " Hz", endpointAddress, rateHz); + return true; +} + +bool UacDevice::getVolumeRangeDb256(int uacVersion, int unitId, int acInterface, int32_t out[3]) { + const uint16_t value = static_cast(kFuVolumeControl << 8); // channel 0 = master + const uint16_t index = static_cast((unitId << 8) | (acInterface & 0xFF)); + + if (uacVersion == 2) { + // RANGE: wNumSubRanges + N × (wMIN, wMAX, wRES); we use the first subrange. + uint8_t buffer[2 + 3 * 2] = {0}; + int read = controlIn(kClassInterfaceIn, kReqRange, value, index, buffer, sizeof(buffer)); + if (read < static_cast(sizeof(buffer))) return false; + const int count = buffer[0] | (buffer[1] << 8); + if (count < 1) return false; + out[0] = static_cast(buffer[2] | (buffer[3] << 8)); + out[1] = static_cast(buffer[4] | (buffer[5] << 8)); + out[2] = static_cast(buffer[6] | (buffer[7] << 8)); + return true; + } + + const uint8_t requests[3] = {kReqGetMin, kReqGetMax, kReqGetRes}; + for (int i = 0; i < 3; ++i) { + uint8_t buffer[2] = {0}; + int read = controlIn(kClassInterfaceIn, requests[i], value, index, buffer, sizeof(buffer)); + if (read < 2) return false; + out[i] = static_cast(buffer[0] | (buffer[1] << 8)); + } + return true; +} + +bool UacDevice::setVolumeDb256(int uacVersion, int unitId, int acInterface, int32_t valueDb256) { + (void)uacVersion; // SET CUR encoding is identical for UAC1 and UAC2 (2-byte value) + const int16_t clamped = static_cast(valueDb256); + const uint8_t data[2] = { + static_cast(clamped & 0xFF), + static_cast((clamped >> 8) & 0xFF), + }; + const uint16_t value = static_cast(kFuVolumeControl << 8); + const uint16_t index = static_cast((unitId << 8) | (acInterface & 0xFF)); + return controlOut(kClassInterfaceOut, kReqCur, value, index, data, sizeof(data)); +} + +bool UacDevice::setMute(int uacVersion, int unitId, int acInterface, bool mute) { + (void)uacVersion; + const uint8_t data[1] = {static_cast(mute ? 1 : 0)}; + const uint16_t value = static_cast(kFuMuteControl << 8); + const uint16_t index = static_cast((unitId << 8) | (acInterface & 0xFF)); + return controlOut(kClassInterfaceOut, kReqCur, value, index, data, sizeof(data)); +} + +bool UacDevice::isHighSpeed() const { + libusb_device* device = libusb_get_device(handle_); + if (device == nullptr) return false; + return libusb_get_device_speed(device) >= LIBUSB_SPEED_HIGH; +} + +std::string UacDevice::lastError() const { + std::lock_guard lock(errorMutex_); + return lastError_; +} + +void UacDevice::setLastError(const std::string& error) { + std::lock_guard lock(errorMutex_); + lastError_ = error; +} + +bool UacDevice::controlOut(uint8_t requestType, uint8_t request, uint16_t value, uint16_t index, + const uint8_t* data, uint16_t length) { + int rc = libusb_control_transfer(handle_, requestType, request, value, index, + const_cast(data), length, kControlTimeoutMs); + if (rc < 0) { + setLastError(std::string("control OUT req=0x") + std::to_string(request) + ": " + + libusb_error_name(rc)); + UA_LOGW("%s", lastError().c_str()); + return false; + } + return true; +} + +int UacDevice::controlIn(uint8_t requestType, uint8_t request, uint16_t value, uint16_t index, + uint8_t* data, uint16_t length) { + int rc = libusb_control_transfer(handle_, requestType, request, value, index, data, length, + kControlTimeoutMs); + if (rc < 0) { + setLastError(std::string("control IN req=0x") + std::to_string(request) + ": " + + libusb_error_name(rc)); + UA_LOGW("%s", lastError().c_str()); + } + return rc; +} diff --git a/usbaudio/src/main/cpp/uac_device.h b/usbaudio/src/main/cpp/uac_device.h new file mode 100644 index 000000000..3d5a585c8 --- /dev/null +++ b/usbaudio/src/main/cpp/uac_device.h @@ -0,0 +1,59 @@ +#pragma once + +#include +#include +#include +#include + +#include "libusb.h" + +/** + * Owns the libusb context/handle wrapped around an Android UsbDeviceConnection fd and + * implements the USB Audio Class control-plane: kernel-driver detach + interface claim, + * alt-setting selection, sample-rate programming (UAC1 endpoint / UAC2 clock source) and + * feature-unit volume/mute. + */ +class UacDevice { +public: + static std::unique_ptr create(int fd, std::string* errorOut); + ~UacDevice(); + + UacDevice(const UacDevice&) = delete; + UacDevice& operator=(const UacDevice&) = delete; + + bool claimInterfaces(int acInterface, int asInterface); + bool setAltSetting(int asInterface, int altSetting); + + bool setSampleRateUac2(int clockId, int acInterface, uint32_t rateHz); + bool setSampleRateUac1(int endpointAddress, uint32_t rateHz); + + /** Volume values are UAC-native: signed 1/256 dB steps. out = {min, max, res}. */ + bool getVolumeRangeDb256(int uacVersion, int unitId, int acInterface, int32_t out[3]); + bool setVolumeDb256(int uacVersion, int unitId, int acInterface, int32_t valueDb256); + bool setMute(int uacVersion, int unitId, int acInterface, bool mute); + + /** True when the bus enumerated the device at high speed (µframe timing). */ + bool isHighSpeed() const; + + libusb_context* context() const { return ctx_; } + libusb_device_handle* handle() const { return handle_; } + + std::string lastError() const; + void setLastError(const std::string& error); + +private: + UacDevice() = default; + + bool controlOut(uint8_t requestType, uint8_t request, uint16_t value, uint16_t index, + const uint8_t* data, uint16_t length); + int controlIn(uint8_t requestType, uint8_t request, uint16_t value, uint16_t index, + uint8_t* data, uint16_t length); + + libusb_context* ctx_ = nullptr; + libusb_device_handle* handle_ = nullptr; + int claimedAc_ = -1; + int claimedAs_ = -1; + + mutable std::mutex errorMutex_; + std::string lastError_; +}; diff --git a/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/UsbAudioNative.kt b/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/UsbAudioNative.kt index 0c77270af..690bd6117 100644 --- a/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/UsbAudioNative.kt +++ b/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/UsbAudioNative.kt @@ -1,11 +1,18 @@ package com.theveloper.pixelplay.usbaudio +import java.nio.ByteBuffer + /** * Thin JNI surface over the native USB Audio Class driver (libusb-based). * * The native library is loaded lazily via [ensureLoaded] so that the normal * (non-exclusive) playback path never pays for it — callers must invoke - * [ensureLoaded] before the first native call. + * [ensureLoaded] before the first native call. [UsbAudioSession] is the only + * intended caller; keep these externals free of business logic. + * + * All methods taking a `handle` expect the value returned by [nativeCreate]; + * a zero handle means creation failed. Return convention: 0 = success, + * negative = failure (details via [nativeGetLastError]). */ object UsbAudioNative { @@ -25,4 +32,90 @@ object UsbAudioNative { /** Returns the version string of the bundled libusb, e.g. "libusb 1.0.30". */ external fun nativeGetVersion(): String + + /** Wraps the UsbDeviceConnection fd; returns an opaque session handle or 0. */ + external fun nativeCreate(fd: Int): Long + + external fun nativeDestroy(handle: Long) + + /** Detaches the kernel driver and claims the AudioControl + AudioStreaming interfaces. */ + external fun nativeClaim(handle: Long, acInterface: Int, asInterface: Int): Int + + /** Selects the alt setting and (re)builds the iso pipeline; call [nativeStart] after. */ + external fun nativeConfigureStream( + handle: Long, + asInterface: Int, + altSetting: Int, + endpointAddress: Int, + feedbackEndpointAddress: Int, + rateHz: Int, + channels: Int, + subslotBytes: Int, + intervalCode: Int, + maxPacketSize: Int, + ringBufferMs: Int + ): Int + + external fun nativeSetSampleRate( + handle: Long, + uacVersion: Int, + clockId: Int, + acInterface: Int, + endpointAddress: Int, + rateHz: Int + ): Int + + external fun nativeStart(handle: Long): Int + external fun nativePause(handle: Long): Int + external fun nativeResume(handle: Long): Int + external fun nativeFlush(handle: Long): Int + + /** Stops streaming and selects alt setting 0, releasing the iso bandwidth. */ + external fun nativeStop(handle: Long, asInterface: Int): Int + + /** + * Queues PCM from a direct [ByteBuffer]. Returns bytes accepted (0 = ring full, + * caller retries later), or negative on error/dead stream. Never blocks. + */ + external fun nativeWrite(handle: Long, buffer: ByteBuffer, offset: Int, size: Int): Int + + /** Frames of real audio in completed transfers — the playback position. */ + external fun nativeGetPlayedFrames(handle: Long): Long + + /** Frames consumed from the ring (played + in flight) — the flush rebase point. */ + external fun nativeGetConsumedFrames(handle: Long): Long + + /** Frames still queued (ring + in flight). */ + external fun nativeGetBufferedFrames(handle: Long): Long + + external fun nativeGetXrunCount(handle: Long): Int + + /** False once the device vanished or the pipeline died. */ + external fun nativeIsAlive(handle: Long): Boolean + + /** [min, max, res] in 1/256 dB, or null when the unit doesn't answer. */ + external fun nativeGetVolumeRangeDb256( + handle: Long, + uacVersion: Int, + unitId: Int, + acInterface: Int + ): IntArray? + + external fun nativeSetVolumeDb256( + handle: Long, + uacVersion: Int, + unitId: Int, + acInterface: Int, + valueDb256: Int + ): Int + + external fun nativeSetMute( + handle: Long, + uacVersion: Int, + unitId: Int, + acInterface: Int, + mute: Boolean + ): Int + + external fun nativeGetLastError(handle: Long): String? } diff --git a/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/UsbAudioSession.kt b/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/UsbAudioSession.kt new file mode 100644 index 000000000..3f831f751 --- /dev/null +++ b/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/UsbAudioSession.kt @@ -0,0 +1,201 @@ +package com.theveloper.pixelplay.usbaudio + +import android.hardware.usb.UsbDeviceConnection +import com.theveloper.pixelplay.usbaudio.descriptor.UacCapabilities +import com.theveloper.pixelplay.usbaudio.descriptor.UacVersion +import com.theveloper.pixelplay.usbaudio.negotiation.NegotiatedFormat +import java.nio.ByteBuffer + +/** + * One exclusive streaming session with a USB DAC. + * + * Owns the native driver handle and the [UsbDeviceConnection] whose file descriptor the + * driver wraps — the connection must stay open for the session's whole life, so the session + * takes ownership and closes it in [close]. + * + * Single-owner: created by the exclusive-mode controller, handed to the audio sink, closed + * exactly once. All methods are safe to call after [close] (they become no-ops / failures) + * so a racing detach can't crash the playback thread. + */ +class UsbAudioSession private constructor( + private val connection: UsbDeviceConnection, + private val handle: Long, + val capabilities: UacCapabilities +) : AutoCloseable { + + private val lock = Any() + private var closed = false + private var claimed = false + + /** The stream configuration currently programmed into the DAC, if any. */ + @Volatile + var currentFormat: NegotiatedFormat? = null + private set + + private val uacVersionCode: Int + get() = if (capabilities.version == UacVersion.UAC2) 2 else 1 + + /** + * Programs [format]: claims interfaces on first use, selects the alt setting, sets the + * sample rate and starts the iso pipeline. Returns false (with [lastError] populated) + * on any failure; the session stays usable for a retry with another format. + */ + fun configure(format: NegotiatedFormat, ringBufferMs: Int = DEFAULT_RING_BUFFER_MS): Boolean { + synchronized(lock) { + if (closed) return false + val candidate = format.candidate + + if (!claimed) { + if (UsbAudioNative.nativeClaim( + handle, capabilities.controlInterfaceNumber, candidate.interfaceNumber + ) != 0 + ) { + return false + } + claimed = true + } + + if (UsbAudioNative.nativeConfigureStream( + handle = handle, + asInterface = candidate.interfaceNumber, + altSetting = candidate.altSetting, + endpointAddress = candidate.endpointAddress, + feedbackEndpointAddress = candidate.feedbackEndpointAddress ?: 0, + rateHz = format.sampleRateHz, + channels = candidate.channels, + subslotBytes = candidate.subslotBytes, + intervalCode = candidate.intervalCode, + maxPacketSize = candidate.maxPacketSize, + ringBufferMs = ringBufferMs + ) != 0 + ) { + return false + } + + if (UsbAudioNative.nativeSetSampleRate( + handle = handle, + uacVersion = uacVersionCode, + clockId = candidate.clockSourceId ?: 0, + acInterface = capabilities.controlInterfaceNumber, + endpointAddress = candidate.endpointAddress, + rateHz = format.sampleRateHz + ) != 0 + ) { + // UAC1 devices without the sampling-frequency control run at a fixed or + // auto-detected rate; treat the failure as non-fatal for them. + if (uacVersionCode == 2 || candidate.uac1SampleRateControl) return false + } + + if (UsbAudioNative.nativeStart(handle) != 0) return false + currentFormat = format + return true + } + } + + /** Queues PCM. Returns bytes accepted (0 = backpressure), negative when the stream died. */ + fun write(buffer: ByteBuffer, offset: Int, size: Int): Int { + if (closed) return -1 + return UsbAudioNative.nativeWrite(handle, buffer, offset, size) + } + + fun pause() { + synchronized(lock) { if (!closed) UsbAudioNative.nativePause(handle) } + } + + fun resume() { + synchronized(lock) { if (!closed) UsbAudioNative.nativeResume(handle) } + } + + fun flush() { + synchronized(lock) { if (!closed) UsbAudioNative.nativeFlush(handle) } + } + + /** Stops streaming and releases iso bandwidth (alt setting 0); format must be re-configured. */ + fun stopStream() { + synchronized(lock) { + if (closed) return + currentFormat?.let { + UsbAudioNative.nativeStop(handle, it.candidate.interfaceNumber) + } + currentFormat = null + } + } + + val playedFrames: Long + get() = if (closed) 0 else UsbAudioNative.nativeGetPlayedFrames(handle) + + val consumedFrames: Long + get() = if (closed) 0 else UsbAudioNative.nativeGetConsumedFrames(handle) + + val bufferedFrames: Long + get() = if (closed) 0 else UsbAudioNative.nativeGetBufferedFrames(handle) + + val xrunCount: Int + get() = if (closed) 0 else UsbAudioNative.nativeGetXrunCount(handle) + + val isAlive: Boolean + get() = !closed && UsbAudioNative.nativeIsAlive(handle) + + val lastError: String? + get() = if (closed) null else UsbAudioNative.nativeGetLastError(handle) + + /** [min, max, res] in 1/256 dB, or null when the DAC has no usable feature unit. */ + fun volumeRangeDb256(): IntArray? { + synchronized(lock) { + if (closed) return null + val unit = capabilities.volume ?: return null + return UsbAudioNative.nativeGetVolumeRangeDb256( + handle, uacVersionCode, unit.featureUnitId, capabilities.controlInterfaceNumber + ) + } + } + + fun setVolumeDb256(valueDb256: Int): Boolean { + synchronized(lock) { + if (closed) return false + val unit = capabilities.volume ?: return false + return UsbAudioNative.nativeSetVolumeDb256( + handle, uacVersionCode, unit.featureUnitId, + capabilities.controlInterfaceNumber, valueDb256 + ) == 0 + } + } + + fun setMute(mute: Boolean): Boolean { + synchronized(lock) { + if (closed) return false + val unit = capabilities.volume ?: return false + if (!unit.hasMasterMute) return false + return UsbAudioNative.nativeSetMute( + handle, uacVersionCode, unit.featureUnitId, + capabilities.controlInterfaceNumber, mute + ) == 0 + } + } + + override fun close() { + synchronized(lock) { + if (closed) return + stopStream() + closed = true + UsbAudioNative.nativeDestroy(handle) + runCatching { connection.close() } + } + } + + companion object { + const val DEFAULT_RING_BUFFER_MS = 250 + + /** + * Wraps an open, permission-granted connection. [capabilities] must come from + * [com.theveloper.pixelplay.usbaudio.descriptor.UacCapabilityProber] on the same device. + * On failure the connection is left untouched (caller keeps ownership). + */ + fun open(connection: UsbDeviceConnection, capabilities: UacCapabilities): UsbAudioSession? { + UsbAudioNative.ensureLoaded() + val handle = UsbAudioNative.nativeCreate(connection.fileDescriptor) + if (handle == 0L) return null + return UsbAudioSession(connection, handle, capabilities) + } + } +} From 86bb61d3bab6722e63b594ba4191fa00c11f174f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 2 Jul 2026 21:19:47 +0000 Subject: [PATCH 05/17] =?UTF-8?q?feat(usb):=20UsbAudioSink=20=E2=80=94=20c?= =?UTF-8?q?ustom=20Media3=20sink=20feeding=20the=20USB=20driver?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PcmRepacker: lossless left-justified subslot packing (16→24/32 zero- pad, float→int exact for ≤24-bit material), truncation without dither only when the DAC offers less depth, mono duplication / silence fill - UsbAudioSink implements the full Media3 AudioSink contract: PCM 16/24/32/float accepted directly (encoded formats rejected so the FFmpeg/MediaCodec renderers keep decoding), negotiation on configure, ring-backpressure via handleBuffer=false (never blocks), position from the driver's played-frames counter with flush rebasing and monotonic clamping, drain semantics from buffered frames, and no volume scaling/speed/effects on the bit-perfect path by design. Conversion paths (5.1→stereo downmix, Sonic 16-bit resample) engage only when the DAC can't take the source format. A dead session (unplug) swallows buffers and notifies the controller instead of crashing playback - Native stream now starts paused (silence primes the DAC clock, no pops) and only counts true mid-stream underruns - 20 JUnit5 tests: golden repack vectors incl. the float round-trip losslessness proof, backpressure, position/flush math, drain, dead- session handling, format negotiation through the sink Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FZMSPh3TgbRAMqgW4q1p6V --- .../data/service/player/usb/PcmRepacker.kt | 127 +++++ .../data/service/player/usb/UsbAudioSink.kt | 467 ++++++++++++++++++ .../service/player/usb/PcmRepackerTest.kt | 119 +++++ .../service/player/usb/UsbAudioSinkTest.kt | 269 ++++++++++ usbaudio/src/main/cpp/iso_stream.cpp | 8 +- usbaudio/src/main/cpp/iso_stream.h | 5 +- 6 files changed, 992 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/com/theveloper/pixelplay/data/service/player/usb/PcmRepacker.kt create mode 100644 app/src/main/java/com/theveloper/pixelplay/data/service/player/usb/UsbAudioSink.kt create mode 100644 app/src/test/java/com/theveloper/pixelplay/data/service/player/usb/PcmRepackerTest.kt create mode 100644 app/src/test/java/com/theveloper/pixelplay/data/service/player/usb/UsbAudioSinkTest.kt diff --git a/app/src/main/java/com/theveloper/pixelplay/data/service/player/usb/PcmRepacker.kt b/app/src/main/java/com/theveloper/pixelplay/data/service/player/usb/PcmRepacker.kt new file mode 100644 index 000000000..25852ca17 --- /dev/null +++ b/app/src/main/java/com/theveloper/pixelplay/data/service/player/usb/PcmRepacker.kt @@ -0,0 +1,127 @@ +package com.theveloper.pixelplay.data.service.player.usb + +import java.nio.ByteBuffer +import java.nio.ByteOrder +import kotlin.math.roundToLong + +/** + * Pure PCM repacking for the USB wire format — the only place samples are ever transformed + * on the bit-perfect path, and by design it only ever: + * - re-justifies samples into the DAC's subslot (left-justified per UAC Type I), which is + * lossless zero-padding when the target depth is >= the source depth; + * - duplicates a mono channel / zero-fills missing channels; + * - truncates (no dither, by policy) when the DAC genuinely offers less depth. + * + * Internally every sample becomes a 32-bit MSB-aligned value ("s32top"); writing the top + * `subslotBytes` bytes little-endian then produces the correct left-justified wire sample + * for any 16/24/32-bit subslot. + */ +object PcmRepacker { + + enum class Encoding(val bytesPerSample: Int) { + PCM_16(2), + /** 3-byte packed little-endian (Media3 C.ENCODING_PCM_24BIT). */ + PCM_24(3), + PCM_32(4), + FLOAT(4) + } + + /** Bytes [repack] will produce for [inputBytes] of source data. */ + fun outputSize(inputBytes: Int, source: Encoding, sourceChannels: Int, targetChannels: Int, subslotBytes: Int): Int { + val frameBytes = source.bytesPerSample * sourceChannels + if (frameBytes == 0) return 0 + val frames = inputBytes / frameBytes + return frames * targetChannels * subslotBytes + } + + /** + * Converts everything between `input.position()` and `input.limit()` into the wire format, + * appending to [output] (which must have enough space — see [outputSize]) in little-endian + * order. Channel handling: equal counts copy through; mono duplicates into the first two + * target channels; missing channels are silence; excess source channels are dropped + * (callers downmix 5.1/7.1 upstream — dropping is only the last-resort fallback). + */ + fun repack( + input: ByteBuffer, + source: Encoding, + sourceChannels: Int, + targetChannels: Int, + subslotBytes: Int, + output: ByteBuffer + ) { + val in_ = input.duplicate().order(ByteOrder.LITTLE_ENDIAN) + output.order(ByteOrder.LITTLE_ENDIAN) + val frameBytes = source.bytesPerSample * sourceChannels + val frames = (in_.limit() - in_.position()) / frameBytes + + val copyChannels = minOf(sourceChannels, targetChannels) + for (frame in 0 until frames) { + val frameBase = in_.position() + frame * frameBytes + for (channel in 0 until targetChannels) { + val sourceChannel = when { + channel < copyChannels -> channel + sourceChannels == 1 && channel == 1 -> 0 // mono → duplicate into R + else -> -1 // silence + } + val s32top = if (sourceChannel >= 0) { + readS32Top(in_, frameBase + sourceChannel * source.bytesPerSample, source) + } else { + 0 + } + writeSubslot(output, s32top, subslotBytes) + } + } + input.position(input.limit()) + } + + /** + * Converts to interleaved 16-bit PCM with the same channel count (truncating, no dither). + * Only used on the already-lossy resample path, never on the bit-perfect path. + */ + fun toPcm16(input: ByteBuffer, source: Encoding, output: ByteBuffer) { + val in_ = input.duplicate().order(ByteOrder.LITTLE_ENDIAN) + output.order(ByteOrder.LITTLE_ENDIAN) + var offset = in_.position() + while (offset + source.bytesPerSample <= in_.limit()) { + val s32top = readS32Top(in_, offset, source) + output.putShort((s32top shr 16).toShort()) + offset += source.bytesPerSample + } + input.position(input.limit()) + } + + /** Reads one sample as a 32-bit MSB-aligned signed value. */ + internal fun readS32Top(buffer: ByteBuffer, offset: Int, source: Encoding): Int = when (source) { + Encoding.PCM_16 -> buffer.getShort(offset).toInt() shl 16 + Encoding.PCM_24 -> { + val u = (buffer.get(offset).toInt() and 0xFF) or + ((buffer.get(offset + 1).toInt() and 0xFF) shl 8) or + ((buffer.get(offset + 2).toInt() and 0xFF) shl 16) + // Sign-extend from bit 23, then left-justify. + (u shl 8) + } + Encoding.PCM_32 -> buffer.getInt(offset) + Encoding.FLOAT -> { + val v = buffer.getFloat(offset) + // Scaling by 2^31 is exponent-only, so ≤24-bit integer material stays exact. + (v.toDouble() * 2147483648.0).roundToLong() + .coerceIn(Int.MIN_VALUE.toLong(), Int.MAX_VALUE.toLong()) + .toInt() + } + } + + /** Writes the top [subslotBytes] bytes of [s32top], little-endian (left-justified sample). */ + internal fun writeSubslot(output: ByteBuffer, s32top: Int, subslotBytes: Int) { + when (subslotBytes) { + 2 -> output.putShort((s32top shr 16).toShort()) + 3 -> { + val v = s32top ushr 8 + output.put((v and 0xFF).toByte()) + output.put(((v shr 8) and 0xFF).toByte()) + output.put(((v shr 16) and 0xFF).toByte()) + } + 4 -> output.putInt(s32top) + else -> throw IllegalArgumentException("Unsupported subslot size: $subslotBytes") + } + } +} diff --git a/app/src/main/java/com/theveloper/pixelplay/data/service/player/usb/UsbAudioSink.kt b/app/src/main/java/com/theveloper/pixelplay/data/service/player/usb/UsbAudioSink.kt new file mode 100644 index 000000000..6e083142f --- /dev/null +++ b/app/src/main/java/com/theveloper/pixelplay/data/service/player/usb/UsbAudioSink.kt @@ -0,0 +1,467 @@ +package com.theveloper.pixelplay.data.service.player.usb + +import androidx.media3.common.AudioAttributes +import androidx.media3.common.AuxEffectInfo +import androidx.media3.common.C +import androidx.media3.common.Format +import androidx.media3.common.PlaybackParameters +import androidx.media3.common.audio.AudioProcessor +import androidx.media3.common.audio.SonicAudioProcessor +import androidx.media3.common.util.UnstableApi +import androidx.media3.exoplayer.audio.AudioSink +import com.theveloper.pixelplay.data.service.player.SurroundDownmixProcessor +import com.theveloper.pixelplay.usbaudio.UsbAudioSession +import com.theveloper.pixelplay.usbaudio.negotiation.FormatNegotiator +import com.theveloper.pixelplay.usbaudio.negotiation.NegotiatedFormat +import com.theveloper.pixelplay.usbaudio.negotiation.SourceFormat +import java.nio.ByteBuffer +import java.nio.ByteOrder +import timber.log.Timber + +/** + * Media3 [AudioSink] that feeds decoded PCM straight to a USB DAC through [UsbAudioSession], + * bypassing AudioTrack/AudioFlinger entirely. + * + * Bit-perfect policy: on the negotiated bit-perfect path samples are only re-justified into + * the DAC's subslot ([PcmRepacker]) — no resampling, no dithering, no volume scaling + * ([setVolume] is a no-op). Conversion (downmix via [SurroundDownmixProcessor], resampling + * via [SonicAudioProcessor] in 16-bit) happens only when the DAC cannot take the source + * format, and is reported through [onFormatChanged]. + * + * A dead session (device unplugged) never throws into the player: buffers are swallowed and + * [onSessionDead] fires so the exclusive-mode controller can pause and fall back gracefully. + */ +@UnstableApi +class UsbAudioSink( + private val session: UsbAudioSession, + private val onFormatChanged: (NegotiatedFormat?, SourceFormat?) -> Unit = { _, _ -> }, + private val onSessionDead: () -> Unit = {} +) : AudioSink { + + private var listener: AudioSink.Listener? = null + + private var inputFormat: Format? = null + private var sourceFormat: SourceFormat? = null + private var negotiated: NegotiatedFormat? = null + private var sourceEncoding: PcmRepacker.Encoding? = null + + /** Conversion pipeline, present only on non-bit-perfect paths. */ + private var downmixProcessor: SurroundDownmixProcessor? = null + private var resampleProcessor: SonicAudioProcessor? = null + + /** Converted wire-format data waiting for ring space (bounded: one input buffer). */ + private var pendingWire: ByteBuffer = EMPTY_BUFFER + private var wireScratch: ByteBuffer = EMPTY_BUFFER + private var pcm16Scratch: ByteBuffer = EMPTY_BUFFER + + private var startMediaTimeUs = 0L + private var startMediaTimeSet = false + private var flushBaseFrames = 0L + private var lastPositionUs = Long.MIN_VALUE + private var draining = false + private var reportedXruns = 0 + private var sessionDeadReported = false + + private var playbackParameters = PlaybackParameters.DEFAULT + private var skipSilenceEnabled = false + private var audioAttributes: AudioAttributes? = null + + // ─── Format support ────────────────────────────────────────────────────── + + override fun setListener(listener: AudioSink.Listener) { + this.listener = listener + } + + override fun supportsFormat(format: Format): Boolean = + getFormatSupport(format) == AudioSink.SINK_FORMAT_SUPPORTED_DIRECTLY + + override fun getFormatSupport(format: Format): Int { + if (format.sampleMimeType != androidx.media3.common.MimeTypes.AUDIO_RAW) { + // No encoded passthrough over USB: keep the renderers decoding. + return AudioSink.SINK_FORMAT_UNSUPPORTED + } + val supportedEncoding = format.pcmEncoding == C.ENCODING_PCM_16BIT || + format.pcmEncoding == C.ENCODING_PCM_24BIT || + format.pcmEncoding == C.ENCODING_PCM_32BIT || + format.pcmEncoding == C.ENCODING_PCM_FLOAT + return if (supportedEncoding && format.channelCount in 1..8 && format.sampleRate > 0) { + AudioSink.SINK_FORMAT_SUPPORTED_DIRECTLY + } else { + AudioSink.SINK_FORMAT_UNSUPPORTED + } + } + + // ─── Configuration ─────────────────────────────────────────────────────── + + @Throws(AudioSink.ConfigurationException::class) + override fun configure(inputFormat: Format, specifiedBufferSize: Int, outputChannels: IntArray?) { + if (getFormatSupport(inputFormat) != AudioSink.SINK_FORMAT_SUPPORTED_DIRECTLY) { + throw AudioSink.ConfigurationException("Unsupported input format", inputFormat) + } + + val encoding = when (inputFormat.pcmEncoding) { + C.ENCODING_PCM_16BIT -> PcmRepacker.Encoding.PCM_16 + C.ENCODING_PCM_24BIT -> PcmRepacker.Encoding.PCM_24 + C.ENCODING_PCM_32BIT -> PcmRepacker.Encoding.PCM_32 + else -> PcmRepacker.Encoding.FLOAT + } + val source = SourceFormat( + sampleRateHz = inputFormat.sampleRate, + bitDepth = when (encoding) { + PcmRepacker.Encoding.PCM_16 -> 16 + PcmRepacker.Encoding.PCM_24 -> 24 + else -> 32 + }, + channels = inputFormat.channelCount, + isFloat = encoding == PcmRepacker.Encoding.FLOAT + ) + + val format = FormatNegotiator.negotiate(source, session.capabilities) + ?: throw AudioSink.ConfigurationException("No negotiable DAC format", inputFormat) + + val sameStream = negotiated == format && session.currentFormat == format + if (!sameStream) { + if (!session.configure(format)) { + throw AudioSink.ConfigurationException( + "DAC rejected ${format.sampleRateHz} Hz alt ${format.candidate.altSetting}: ${session.lastError}", + inputFormat + ) + } + } + + this.inputFormat = inputFormat + this.sourceFormat = source + this.sourceEncoding = encoding + this.negotiated = format + buildConversionPipeline(source, encoding, format) + + pendingWire = EMPTY_BUFFER + startMediaTimeSet = false + lastPositionUs = Long.MIN_VALUE + flushBaseFrames = session.consumedFrames + draining = false + sessionDeadReported = false + + Timber.tag(TAG).i( + "Configured: %d Hz/%d-bit/%dch → DAC alt %d %d Hz/%d-bit (bitPerfect=%b)", + source.sampleRateHz, source.bitDepth, source.channels, + format.candidate.altSetting, format.sampleRateHz, format.candidate.bitResolution, + format.conversion.isBitPerfect + ) + onFormatChanged(format, source) + } + + private fun buildConversionPipeline( + source: SourceFormat, + encoding: PcmRepacker.Encoding, + format: NegotiatedFormat + ) { + downmixProcessor = null + resampleProcessor = null + + var stageRate = source.sampleRateHz + var stageChannels = source.channels + var stageEncoding = encoding + + if (format.conversion.downmixed && (source.channels == 6 || source.channels == 8) && + (encoding == PcmRepacker.Encoding.PCM_16 || encoding == PcmRepacker.Encoding.FLOAT) + ) { + val processor = SurroundDownmixProcessor() + processor.configure( + AudioProcessor.AudioFormat( + stageRate, stageChannels, + if (encoding == PcmRepacker.Encoding.FLOAT) C.ENCODING_PCM_FLOAT else C.ENCODING_PCM_16BIT + ) + ) + processor.flush() + downmixProcessor = processor + stageChannels = 2 + } + + if (format.conversion.resampled) { + // Resampling is inherently lossy; Sonic runs in 16-bit, so this already-converted + // path drops to 16-bit first. The bit-perfect path never comes through here. + val processor = SonicAudioProcessor() + processor.setOutputSampleRateHz(format.sampleRateHz) + processor.configure( + AudioProcessor.AudioFormat(stageRate, stageChannels, C.ENCODING_PCM_16BIT) + ) + processor.flush() + resampleProcessor = processor + stageEncoding = PcmRepacker.Encoding.PCM_16 + } + } + + // ─── Data path ─────────────────────────────────────────────────────────── + + override fun handleBuffer( + buffer: ByteBuffer, + presentationTimeUs: Long, + encodedAccessUnitCount: Int + ): Boolean { + val format = negotiated ?: return true.also { buffer.position(buffer.limit()) } + + if (!session.isAlive) { + reportSessionDeadOnce() + buffer.position(buffer.limit()) // swallow; controller is falling back + return true + } + + if (!drainPendingWire()) return false + + if (!startMediaTimeSet) { + startMediaTimeUs = presentationTimeUs + startMediaTimeSet = true + } + + convertToWire(buffer, format) + drainPendingWire() + return true + } + + /** Converts the whole input buffer into [pendingWire] (wire format, direct buffer). */ + private fun convertToWire(buffer: ByteBuffer, format: NegotiatedFormat) { + val encoding = sourceEncoding ?: return + val source = sourceFormat ?: return + var stage: ByteBuffer = buffer + var stageEncoding = encoding + var stageChannels = source.channels + + downmixProcessor?.let { processor -> + stage = pushThrough(processor, stage) + stageChannels = 2 + } + + resampleProcessor?.let { processor -> + if (stageEncoding != PcmRepacker.Encoding.PCM_16) { + val needed = (stage.remaining() / stageEncoding.bytesPerSample) * 2 + pcm16Scratch = ensureCapacity(pcm16Scratch, needed) + pcm16Scratch.clear() + PcmRepacker.toPcm16(stage, stageEncoding, pcm16Scratch) + pcm16Scratch.flip() + stage = pcm16Scratch + stageEncoding = PcmRepacker.Encoding.PCM_16 + } + stage = pushThrough(processor, stage) + } + + val candidate = format.candidate + val outBytes = PcmRepacker.outputSize( + stage.remaining(), stageEncoding, stageChannels, candidate.channels, candidate.subslotBytes + ) + // Keep whatever wasn't flushed yet and append (bounded: ring backpressure stops the + // renderer from feeding more than ~2 buffers ahead). + val keep = pendingWire.remaining() + wireScratch = ensureCapacity(wireScratch, keep + outBytes) + wireScratch.clear() + if (keep > 0) wireScratch.put(pendingWire) + PcmRepacker.repack( + stage, stageEncoding, stageChannels, candidate.channels, candidate.subslotBytes, wireScratch + ) + wireScratch.flip() + // Swap scratch and pending so the next round reuses the other buffer. + val previousPending = pendingWire + pendingWire = wireScratch + wireScratch = if (previousPending === EMPTY_BUFFER) EMPTY_BUFFER else previousPending + + buffer.position(buffer.limit()) + } + + /** + * Runs [input] fully through an [AudioProcessor], accumulating every output chunk. + * Only used on conversion paths (downmix/resample) — never on the bit-perfect path. + */ + private fun pushThrough(processor: AudioProcessor, input: ByteBuffer): ByteBuffer { + var accumulator = ByteBuffer.allocate(maxOf(input.remaining() * 2, 4096)) + fun append(chunk: ByteBuffer) { + if (chunk.remaining() > accumulator.remaining()) { + val grown = ByteBuffer.allocate((accumulator.position() + chunk.remaining()) * 2) + accumulator.flip() + grown.put(accumulator) + accumulator = grown + } + accumulator.put(chunk) + } + while (input.hasRemaining()) { + processor.queueInput(input) + while (true) { + val out = processor.output + if (!out.hasRemaining()) break + append(out) + } + } + while (true) { + val out = processor.output + if (!out.hasRemaining()) break + append(out) + } + accumulator.flip() + return accumulator.order(ByteOrder.LITTLE_ENDIAN) + } + + /** Returns true when nothing is left pending. */ + private fun drainPendingWire(): Boolean { + if (!pendingWire.hasRemaining()) return true + if (!session.isAlive) { + reportSessionDeadOnce() + pendingWire.position(pendingWire.limit()) + return true + } + val written = session.write(pendingWire, pendingWire.position(), pendingWire.remaining()) + if (written < 0) { + reportSessionDeadOnce() + pendingWire.position(pendingWire.limit()) + return true + } + if (written > 0) { + pendingWire.position(pendingWire.position() + written) + } + maybeReportUnderruns() + return !pendingWire.hasRemaining() + } + + private fun reportSessionDeadOnce() { + if (!sessionDeadReported) { + sessionDeadReported = true + Timber.tag(TAG).w("USB session dead: %s", session.lastError ?: "device detached") + onSessionDead() + } + } + + private fun maybeReportUnderruns() { + val xruns = session.xrunCount + if (xruns > reportedXruns) { + val bufferSizeMs = UsbAudioSession.DEFAULT_RING_BUFFER_MS.toLong() + listener?.onUnderrun((xruns - reportedXruns), bufferSizeMs, bufferSizeMs) + reportedXruns = xruns + } + } + + // ─── Position ──────────────────────────────────────────────────────────── + + override fun getCurrentPositionUs(sourceEnded: Boolean): Long { + val format = negotiated ?: return AudioSink.CURRENT_POSITION_NOT_SET + val source = sourceFormat ?: return AudioSink.CURRENT_POSITION_NOT_SET + if (!startMediaTimeSet) return AudioSink.CURRENT_POSITION_NOT_SET + + val playedOutputFrames = (session.playedFrames - flushBaseFrames).coerceAtLeast(0) + // Map DAC frames back to media time; with resampling the output rate differs from + // the source rate, so scale through the output rate directly. + val mediaTimeUs = startMediaTimeUs + + playedOutputFrames * 1_000_000L / format.sampleRateHz.coerceAtLeast(1) + + // Monotonic within one rebase epoch (seek/flush resets). + val clamped = if (lastPositionUs != Long.MIN_VALUE) maxOf(lastPositionUs, mediaTimeUs) else mediaTimeUs + lastPositionUs = clamped + return clamped + } + + override fun handleDiscontinuity() { + // Rebase on the next buffer's presentation time (gapless transitions). + startMediaTimeSet = false + lastPositionUs = Long.MIN_VALUE + } + + // ─── Transport ─────────────────────────────────────────────────────────── + + override fun play() { + session.resume() + } + + override fun pause() { + session.pause() + } + + @Throws(AudioSink.WriteException::class) + override fun playToEndOfStream() { + draining = true + drainPendingWire() + } + + override fun isEnded(): Boolean { + if (!draining) return false + drainPendingWire() + return !pendingWire.hasRemaining() && session.bufferedFrames == 0L + } + + override fun hasPendingData(): Boolean { + if (!session.isAlive) return false + drainPendingWire() + return pendingWire.hasRemaining() || session.bufferedFrames > 0L + } + + override fun flush() { + session.flush() + pendingWire = EMPTY_BUFFER + downmixProcessor?.flush() + resampleProcessor?.flush() + flushBaseFrames = session.consumedFrames + startMediaTimeSet = false + lastPositionUs = Long.MIN_VALUE + draining = false + } + + override fun reset() { + flush() + inputFormat = null + sourceFormat = null + sourceEncoding = null + negotiated = null + downmixProcessor = null + resampleProcessor = null + onFormatChanged(null, null) + // The session itself is owned by the exclusive-mode controller, not the sink. + } + + // ─── Bit-perfect no-ops and bookkeeping ────────────────────────────────── + + override fun setPlaybackParameters(playbackParameters: PlaybackParameters) { + // Speed/pitch processing would break bit-perfect output; exclusive mode plays 1×. + if (playbackParameters != PlaybackParameters.DEFAULT) { + Timber.tag(TAG).i("Ignoring playback parameters %s in USB exclusive mode", playbackParameters) + } + this.playbackParameters = PlaybackParameters.DEFAULT + } + + override fun getPlaybackParameters(): PlaybackParameters = playbackParameters + + override fun setSkipSilenceEnabled(skipSilenceEnabled: Boolean) { + this.skipSilenceEnabled = false // not supported on the bit-perfect path + } + + override fun getSkipSilenceEnabled(): Boolean = skipSilenceEnabled + + override fun setAudioAttributes(audioAttributes: AudioAttributes) { + this.audioAttributes = audioAttributes // stored; no AudioTrack to apply them to + } + + override fun getAudioAttributes(): AudioAttributes? = audioAttributes + + override fun setAudioSessionId(audioSessionId: Int) = Unit // no platform audio session + + override fun setAuxEffectInfo(auxEffectInfo: AuxEffectInfo) = Unit // no effects by design + + override fun getAudioTrackBufferSizeUs(): Long = + if (negotiated != null) UsbAudioSession.DEFAULT_RING_BUFFER_MS * 1000L else C.TIME_UNSET + + override fun enableTunnelingV21() = Unit + + override fun disableTunneling() = Unit + + override fun setVolume(volume: Float) { + // Safety net: ReplayGain/crossfade are disabled upstream while exclusive. + if (volume != 1f) { + Timber.tag(TAG).w("Ignoring setVolume(%f) — no volume scaling in bit-perfect path", volume) + } + } + + private companion object { + const val TAG = "UsbAudioSink" + val EMPTY_BUFFER: ByteBuffer = ByteBuffer.allocateDirect(0).order(ByteOrder.LITTLE_ENDIAN) + + fun ensureCapacity(buffer: ByteBuffer, needed: Int): ByteBuffer = + if (buffer.capacity() >= needed) buffer + else ByteBuffer.allocateDirect(Integer.highestOneBit(needed.coerceAtLeast(1)) * 2) + .order(ByteOrder.LITTLE_ENDIAN) + } +} diff --git a/app/src/test/java/com/theveloper/pixelplay/data/service/player/usb/PcmRepackerTest.kt b/app/src/test/java/com/theveloper/pixelplay/data/service/player/usb/PcmRepackerTest.kt new file mode 100644 index 000000000..0f5ce96c0 --- /dev/null +++ b/app/src/test/java/com/theveloper/pixelplay/data/service/player/usb/PcmRepackerTest.kt @@ -0,0 +1,119 @@ +package com.theveloper.pixelplay.data.service.player.usb + +import com.google.common.truth.Truth.assertThat +import java.nio.ByteBuffer +import java.nio.ByteOrder +import org.junit.jupiter.api.Test + +class PcmRepackerTest { + + private fun buffer(vararg bytes: Int): ByteBuffer = + ByteBuffer.wrap(bytes.map { it.toByte() }.toByteArray()).order(ByteOrder.LITTLE_ENDIAN) + + private fun out(size: Int): ByteBuffer = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN) + + private fun ByteBuffer.bytes(): List { + val result = ArrayList(position()) + for (i in 0 until position()) result += get(i).toInt() and 0xFF + return result + } + + @Test + fun `s16 to 24-bit subslot zero-pads the LSB`() { + // One stereo frame: L=0x1234, R=0xFEDC (-292) + val input = buffer(0x34, 0x12, 0xDC, 0xFE) + val output = out(6) + PcmRepacker.repack(input, PcmRepacker.Encoding.PCM_16, 2, 2, 3, output) + assertThat(output.bytes()).containsExactly(0x00, 0x34, 0x12, 0x00, 0xDC, 0xFE).inOrder() + } + + @Test + fun `s16 to 16-bit subslot is a pass-through`() { + val input = buffer(0x34, 0x12, 0xDC, 0xFE) + val output = out(4) + PcmRepacker.repack(input, PcmRepacker.Encoding.PCM_16, 2, 2, 2, output) + assertThat(output.bytes()).containsExactly(0x34, 0x12, 0xDC, 0xFE).inOrder() + } + + @Test + fun `s24 to 32-bit subslot left-justifies`() { + // One mono sample 0x123456 → 0x12345600 LE + val input = buffer(0x56, 0x34, 0x12) + val output = out(4) + PcmRepacker.repack(input, PcmRepacker.Encoding.PCM_24, 1, 1, 4, output) + assertThat(output.bytes()).containsExactly(0x00, 0x56, 0x34, 0x12).inOrder() + } + + @Test + fun `s24 negative sample keeps its sign`() { + // 0x800000 = most negative 24-bit value + val input = buffer(0x00, 0x00, 0x80) + assertThat(PcmRepacker.readS32Top(input, 0, PcmRepacker.Encoding.PCM_24)) + .isEqualTo(Int.MIN_VALUE) + } + + @Test + fun `float golden values`() { + fun floatToS32(v: Float): Int { + val b = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putFloat(0, v) + return PcmRepacker.readS32Top(b, 0, PcmRepacker.Encoding.FLOAT) + } + assertThat(floatToS32(0f)).isEqualTo(0) + assertThat(floatToS32(0.5f)).isEqualTo(0x40000000) + assertThat(floatToS32(-1.0f)).isEqualTo(Int.MIN_VALUE) + assertThat(floatToS32(1.0f)).isEqualTo(Int.MAX_VALUE) // clamped + assertThat(floatToS32(2.0f)).isEqualTo(Int.MAX_VALUE) // clipped + } + + @Test + fun `24-bit int to float and back is the identity - the bit-perfect claim`() { + // Every value that a 24-bit source can produce must survive int→float→wire intact. + val samples = intArrayOf( + -(1 shl 23), -(1 shl 23) + 1, -1, 0, 1, 42, 0x123456, (1 shl 23) - 1 + ) + for (s24 in samples) { + val asFloat = s24.toFloat() / (1 shl 23) + val floatBytes = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putFloat(0, asFloat) + val s32top = PcmRepacker.readS32Top(floatBytes, 0, PcmRepacker.Encoding.FLOAT) + + val wire = out(3) + PcmRepacker.writeSubslot(wire, s32top, 3) + val recovered = (wire.get(0).toInt() and 0xFF) or + ((wire.get(1).toInt() and 0xFF) shl 8) or + (wire.get(2).toInt() shl 16) // sign-extends via plain Int shl of signed byte + assertThat(recovered).isEqualTo(s24) + } + } + + @Test + fun `s32 to 16-bit subslot truncates without dither`() { + val input = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(0, 0x1234ABCD) + input.position(0).limit(4) + val output = out(2) + PcmRepacker.repack(input, PcmRepacker.Encoding.PCM_32, 1, 1, 2, output) + assertThat(output.bytes()).containsExactly(0x34, 0x12).inOrder() + } + + @Test + fun `mono duplicates into both stereo channels`() { + val input = buffer(0x34, 0x12) + val output = out(4) + PcmRepacker.repack(input, PcmRepacker.Encoding.PCM_16, 1, 2, 2, output) + assertThat(output.bytes()).containsExactly(0x34, 0x12, 0x34, 0x12).inOrder() + } + + @Test + fun `outputSize accounts for channel and subslot changes`() { + // 10 stereo s16 frames (40 bytes) → stereo 4-byte subslot = 80 bytes + assertThat(PcmRepacker.outputSize(40, PcmRepacker.Encoding.PCM_16, 2, 2, 4)).isEqualTo(80) + // 10 mono float frames (40 bytes) → stereo 3-byte subslot = 60 bytes + assertThat(PcmRepacker.outputSize(40, PcmRepacker.Encoding.FLOAT, 1, 2, 3)).isEqualTo(60) + } + + @Test + fun `repack consumes the input buffer`() { + val input = buffer(0x34, 0x12) + PcmRepacker.repack(input, PcmRepacker.Encoding.PCM_16, 1, 1, 2, out(2)) + assertThat(input.hasRemaining()).isFalse() + } +} diff --git a/app/src/test/java/com/theveloper/pixelplay/data/service/player/usb/UsbAudioSinkTest.kt b/app/src/test/java/com/theveloper/pixelplay/data/service/player/usb/UsbAudioSinkTest.kt new file mode 100644 index 000000000..f7be4fea0 --- /dev/null +++ b/app/src/test/java/com/theveloper/pixelplay/data/service/player/usb/UsbAudioSinkTest.kt @@ -0,0 +1,269 @@ +package com.theveloper.pixelplay.data.service.player.usb + +import androidx.media3.common.C +import androidx.media3.common.Format +import androidx.media3.common.MimeTypes +import androidx.media3.exoplayer.audio.AudioSink +import com.google.common.truth.Truth.assertThat +import com.theveloper.pixelplay.usbaudio.UsbAudioSession +import com.theveloper.pixelplay.usbaudio.descriptor.EndpointSyncType +import com.theveloper.pixelplay.usbaudio.descriptor.FormatCandidate +import com.theveloper.pixelplay.usbaudio.descriptor.UacCapabilities +import com.theveloper.pixelplay.usbaudio.descriptor.UacVersion +import com.theveloper.pixelplay.usbaudio.negotiation.NegotiatedFormat +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import java.nio.ByteBuffer +import java.nio.ByteOrder +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class UsbAudioSinkTest { + + private val capabilities = UacCapabilities( + version = UacVersion.UAC2, + controlInterfaceNumber = 0, + formats = listOf( + FormatCandidate( + interfaceNumber = 1, altSetting = 1, channels = 2, subslotBytes = 2, + bitResolution = 16, sampleRatesHz = listOf(44_100, 48_000, 96_000), + endpointAddress = 0x01, maxPacketSize = 512, intervalCode = 1, + syncType = EndpointSyncType.ASYNCHRONOUS, feedbackEndpointAddress = 0x81, + clockSourceId = 0x29, uac1SampleRateControl = false + ), + FormatCandidate( + interfaceNumber = 1, altSetting = 2, channels = 2, subslotBytes = 4, + bitResolution = 32, sampleRatesHz = listOf(44_100, 48_000, 96_000), + endpointAddress = 0x01, maxPacketSize = 1024, intervalCode = 1, + syncType = EndpointSyncType.ASYNCHRONOUS, feedbackEndpointAddress = 0x81, + clockSourceId = 0x29, uac1SampleRateControl = false + ) + ), + volume = null + ) + + private lateinit var session: UsbAudioSession + private var alive = true + private var acceptBytes = Int.MAX_VALUE + private val written = mutableListOf() + private var playedFrames = 0L + private var consumedFrames = 0L + private var bufferedFrames = 0L + private var configuredFormat: NegotiatedFormat? = null + + @BeforeEach + fun setUp() { + alive = true + acceptBytes = Int.MAX_VALUE + written.clear() + playedFrames = 0L + consumedFrames = 0L + bufferedFrames = 0L + configuredFormat = null + + session = mockk(relaxed = true) { + every { capabilities } returns this@UsbAudioSinkTest.capabilities + every { isAlive } answers { alive } + every { currentFormat } answers { configuredFormat } + every { configure(any(), any()) } answers { + configuredFormat = firstArg() + true + } + every { configure(any()) } answers { + configuredFormat = firstArg() + true + } + every { playedFrames } answers { this@UsbAudioSinkTest.playedFrames } + every { consumedFrames } answers { this@UsbAudioSinkTest.consumedFrames } + every { bufferedFrames } answers { this@UsbAudioSinkTest.bufferedFrames } + every { xrunCount } returns 0 + every { lastError } returns null + every { write(any(), any(), any()) } answers { + val buffer = firstArg() + val offset = secondArg() + val size = thirdArg() + if (!alive) -1 + else { + val accepted = minOf(size, acceptBytes) + val copy = ByteArray(accepted) + val dup = buffer.duplicate() + dup.position(offset) + dup.get(copy) + if (accepted > 0) written += copy + accepted + } + } + } + } + + private fun sink( + onFormatChanged: (NegotiatedFormat?, com.theveloper.pixelplay.usbaudio.negotiation.SourceFormat?) -> Unit = { _, _ -> }, + onSessionDead: () -> Unit = {} + ) = UsbAudioSink(session, onFormatChanged, onSessionDead) + + private fun pcm16Format(rate: Int = 44_100, channels: Int = 2): Format = Format.Builder() + .setSampleMimeType(MimeTypes.AUDIO_RAW) + .setPcmEncoding(C.ENCODING_PCM_16BIT) + .setSampleRate(rate) + .setChannelCount(channels) + .build() + + private fun directBuffer(vararg bytes: Int): ByteBuffer { + val buffer = ByteBuffer.allocateDirect(bytes.size).order(ByteOrder.LITTLE_ENDIAN) + bytes.forEach { buffer.put(it.toByte()) } + buffer.flip() + return buffer + } + + @Test + fun `rejects encoded formats so renderers keep decoding`() { + val aac = Format.Builder().setSampleMimeType(MimeTypes.AUDIO_AAC).build() + assertThat(sink().getFormatSupport(aac)).isEqualTo(AudioSink.SINK_FORMAT_UNSUPPORTED) + } + + @Test + fun `supports pcm 16 24 32 and float`() { + for (encoding in intArrayOf( + C.ENCODING_PCM_16BIT, C.ENCODING_PCM_24BIT, C.ENCODING_PCM_32BIT, C.ENCODING_PCM_FLOAT + )) { + val format = Format.Builder() + .setSampleMimeType(MimeTypes.AUDIO_RAW) + .setPcmEncoding(encoding) + .setSampleRate(48_000) + .setChannelCount(2) + .build() + assertThat(sink().getFormatSupport(format)) + .isEqualTo(AudioSink.SINK_FORMAT_SUPPORTED_DIRECTLY) + } + } + + @Test + fun `configure negotiates a bit-perfect format and reports it`() { + var reported: NegotiatedFormat? = null + val sink = sink(onFormatChanged = { format, _ -> reported = format }) + + sink.configure(pcm16Format(rate = 44_100), 0, null) + + assertThat(configuredFormat).isNotNull() + assertThat(configuredFormat!!.sampleRateHz).isEqualTo(44_100) + assertThat(configuredFormat!!.candidate.bitResolution).isEqualTo(16) + assertThat(configuredFormat!!.conversion.isBitPerfect).isTrue() + assertThat(reported).isEqualTo(configuredFormat) + } + + @Test + fun `handleBuffer repacks and writes to the session`() { + val sink = sink() + sink.configure(pcm16Format(), 0, null) + + val consumed = sink.handleBuffer(directBuffer(0x34, 0x12, 0xDC, 0xFE), 0L, 1) + + assertThat(consumed).isTrue() + assertThat(written.single().toList().map { it.toInt() and 0xFF }) + .containsExactly(0x34, 0x12, 0xDC, 0xFE).inOrder() + } + + @Test + fun `full ring backpressures without losing data`() { + val sink = sink() + sink.configure(pcm16Format(), 0, null) + + acceptBytes = 2 // ring only takes 2 bytes per write call + assertThat(sink.handleBuffer(directBuffer(0x01, 0x02, 0x03, 0x04), 0L, 1)).isTrue() + + // Next buffer can't go in until pending wire data drains. + acceptBytes = 0 + val next = directBuffer(0x05, 0x06, 0x07, 0x08) + assertThat(sink.handleBuffer(next, 1000L, 1)).isFalse() + assertThat(next.remaining()).isEqualTo(4) // untouched + + acceptBytes = Int.MAX_VALUE + assertThat(sink.handleBuffer(next, 1000L, 1)).isTrue() + assertThat(written.flatMap { it.toList() }.map { it.toInt() and 0xFF }) + .containsExactly(0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08).inOrder() + } + + @Test + fun `position advances with played frames and rebases on flush`() { + val sink = sink() + sink.configure(pcm16Format(rate = 44_100), 0, null) + + assertThat(sink.getCurrentPositionUs(false)).isEqualTo(AudioSink.CURRENT_POSITION_NOT_SET) + + sink.handleBuffer(directBuffer(0, 0, 0, 0), 500_000L, 1) + playedFrames = 44_100 // one second played + assertThat(sink.getCurrentPositionUs(false)).isEqualTo(500_000L + 1_000_000L) + + // Seek: flush snapshots consumed frames as the new base. + consumedFrames = 44_100 + sink.flush() + playedFrames = 44_100 + sink.handleBuffer(directBuffer(0, 0, 0, 0), 30_000_000L, 1) + assertThat(sink.getCurrentPositionUs(false)).isEqualTo(30_000_000L) + } + + @Test + fun `isEnded only after drain completes`() { + val sink = sink() + sink.configure(pcm16Format(), 0, null) + sink.handleBuffer(directBuffer(0, 0, 0, 0), 0L, 1) + + bufferedFrames = 10 + sink.playToEndOfStream() + assertThat(sink.isEnded()).isFalse() + + bufferedFrames = 0 + assertThat(sink.isEnded()).isTrue() + } + + @Test + fun `dead session swallows buffers and notifies once`() { + var deadCalls = 0 + val sink = sink(onSessionDead = { deadCalls++ }) + sink.configure(pcm16Format(), 0, null) + + alive = false + val buffer = directBuffer(1, 2, 3, 4) + assertThat(sink.handleBuffer(buffer, 0L, 1)).isTrue() + assertThat(buffer.hasRemaining()).isFalse() + assertThat(sink.handleBuffer(directBuffer(5, 6), 0L, 1)).isTrue() + assertThat(deadCalls).isEqualTo(1) + } + + @Test + fun `volume changes are ignored on the bit-perfect path`() { + val sink = sink() + sink.configure(pcm16Format(), 0, null) + sink.setVolume(0.5f) // must not touch the session or the PCM path + sink.handleBuffer(directBuffer(0x34, 0x12, 0xDC, 0xFE), 0L, 1) + assertThat(written.single().toList().map { it.toInt() and 0xFF }) + .containsExactly(0x34, 0x12, 0xDC, 0xFE).inOrder() + } + + @Test + fun `hi-res float input picks the 32-bit alt setting`() { + val sink = sink() + val format = Format.Builder() + .setSampleMimeType(MimeTypes.AUDIO_RAW) + .setPcmEncoding(C.ENCODING_PCM_FLOAT) + .setSampleRate(96_000) + .setChannelCount(2) + .build() + sink.configure(format, 0, null) + + assertThat(configuredFormat!!.candidate.subslotBytes).isEqualTo(4) + assertThat(configuredFormat!!.sampleRateHz).isEqualTo(96_000) + assertThat(configuredFormat!!.conversion.isBitPerfect).isTrue() + + // 0.5f left channel, -1.0f right → left-justified 32-bit wire samples + val floats = ByteBuffer.allocateDirect(8).order(ByteOrder.LITTLE_ENDIAN) + floats.putFloat(0.5f).putFloat(-1.0f) + floats.flip() + sink.handleBuffer(floats, 0L, 1) + + val wire = ByteBuffer.wrap(written.single()).order(ByteOrder.LITTLE_ENDIAN) + assertThat(wire.int).isEqualTo(0x40000000) + assertThat(wire.int).isEqualTo(Int.MIN_VALUE) + } +} diff --git a/usbaudio/src/main/cpp/iso_stream.cpp b/usbaudio/src/main/cpp/iso_stream.cpp index 6bdc41e26..173cd6d9d 100644 --- a/usbaudio/src/main/cpp/iso_stream.cpp +++ b/usbaudio/src/main/cpp/iso_stream.cpp @@ -177,10 +177,14 @@ void IsoStream::fillAndSubmit(TransferContext& context) { if (!silent && bytes > 0) { got = ring_->read(cursor, bytes); if (got < bytes) { - // Underrun: pad with silence. Only count once per packet while playing. std::memset(cursor + got, 0, bytes - got); - xruns_.fetch_add(1, std::memory_order_relaxed); + // Count as an underrun only when audio actually ran dry mid-stream — + // steady silence at a track boundary / drain is not an xrun. + if (got > 0 || lastPacketHadData_) { + xruns_.fetch_add(1, std::memory_order_relaxed); + } } + lastPacketHadData_ = got > 0; } else if (bytes > 0) { std::memset(cursor, 0, bytes); } diff --git a/usbaudio/src/main/cpp/iso_stream.h b/usbaudio/src/main/cpp/iso_stream.h index ac45d45b6..720734d8d 100644 --- a/usbaudio/src/main/cpp/iso_stream.h +++ b/usbaudio/src/main/cpp/iso_stream.h @@ -100,6 +100,7 @@ class IsoStream { uint64_t nominalRateQ16_ = 0; std::atomic feedbackRateQ16_{0}; // 0 = no feedback yet uint64_t rateAccumulatorQ16_ = 0; // event-thread only + bool lastPacketHadData_ = false; // event-thread only std::unique_ptr ring_; std::vector> transfers_; @@ -111,7 +112,9 @@ class IsoStream { std::thread eventThread_; std::atomic running_{false}; std::atomic stopping_{false}; - std::atomic paused_{false}; + // Streams begin paused: silence flows (locking the DAC's PLL, priming against pops) + // without consuming the ring or counting xruns until resume(). + std::atomic paused_{true}; std::atomic dead_{false}; std::atomic playedFrames_{0}; From ccfd4da2749d7ac5b1705118a82d864bdefa6f3b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Jul 2026 06:45:24 +0000 Subject: [PATCH 06/17] feat(usb): exclusive-mode controller and engine/service integration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - UsbExclusiveModeController (@Singleton): owns the state machine (Disabled/NoDevice/DeviceDetected/PermissionPending/PermissionDenied/ Ready/Active/Error) and the session lifecycle — combines the mode preference with attached devices and permission events; on grant it opens the device, parses descriptors, probes capabilities and establishes the UsbAudioSession; auto-remembers granted devices; detach or driver death closes the session exactly once and emits sessionLost - DualPlayerEngine.setUsbExclusiveMode(): rebuilds players preserving queue/position (setHiFiMode pattern); buildAudioSink() branches to UsbAudioSink while a session is set (normal path byte-for-byte unchanged otherwise); audio offload forced off and crossfade prepare skipped while exclusive - TransitionController treats exclusive mode like crossfade-disabled (plain gapless advance) via the engine's usbExclusiveActive flow - MusicService: collects controller state to engage/release the engine, pauses on unplug and auto-resumes when a remembered device with auto-resume re-attaches; ReplayGain (player-volume scaling) is gated off while exclusive is active - Controller transition-table tests (Turbine + MockK) incl. deny, detach-while-ready, disable-while-ready, unopenable device, Ready↔Active via sink callbacks Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FZMSPh3TgbRAMqgW4q1p6V --- .../pixelplay/data/service/MusicService.kt | 57 +++- .../data/service/player/DualPlayerEngine.kt | 50 ++- .../service/player/TransitionController.kt | 19 +- .../data/usb/UsbExclusiveModeController.kt | 312 ++++++++++++++++++ .../com/theveloper/pixelplay/di/AppModule.kt | 8 + .../usb/UsbExclusiveModeControllerTest.kt | 252 ++++++++++++++ 6 files changed, 689 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/com/theveloper/pixelplay/data/usb/UsbExclusiveModeController.kt create mode 100644 app/src/test/java/com/theveloper/pixelplay/data/usb/UsbExclusiveModeControllerTest.kt diff --git a/app/src/main/java/com/theveloper/pixelplay/data/service/MusicService.kt b/app/src/main/java/com/theveloper/pixelplay/data/service/MusicService.kt index adab461ca..2ab97f5fb 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/service/MusicService.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/service/MusicService.kt @@ -53,6 +53,8 @@ import com.theveloper.pixelplay.data.preferences.ThemePreferencesRepository import com.theveloper.pixelplay.data.preferences.UserPreferencesRepository import com.theveloper.pixelplay.data.repository.MusicRepository import com.theveloper.pixelplay.data.service.player.DualPlayerEngine +import com.theveloper.pixelplay.data.usb.UsbExclusiveModeController +import com.theveloper.pixelplay.data.usb.UsbExclusiveState import com.theveloper.pixelplay.data.service.player.TransitionController import com.theveloper.pixelplay.ui.glancewidget.PlayerActions import com.theveloper.pixelplay.utils.AlbumArtUtils @@ -63,6 +65,8 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -168,6 +172,8 @@ class MusicService : MediaLibraryService() { @Inject lateinit var listeningStatsTracker: ListeningStatsTracker @Inject + lateinit var usbExclusiveModeController: UsbExclusiveModeController + @Inject @AppScope lateinit var appScope: CoroutineScope @@ -188,6 +194,9 @@ class MusicService : MediaLibraryService() { private val controllerLastBrowsedParent = mutableMapOf() private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) private var keepPlayingInBackground = true + // Set when the USB DAC vanished while playing; a re-attach of a remembered device + // with auto-resume then continues playback. + private var usbResumeAfterReattach = false private var isManualShuffleEnabled = false private var persistentShuffleEnabled = false // Holds the previous main-thread UncaughtExceptionHandler so we can restore it in onDestroy. @@ -507,6 +516,42 @@ class MusicService : MediaLibraryService() { } } + // USB exclusive mode: attach/detach the bit-perfect sink as the controller's session + // comes and goes. The engine rebuild preserves queue/position (same as Hi-Fi toggle). + serviceScope.launch { + usbExclusiveModeController.state.collect { state -> + val session = when (state) { + is UsbExclusiveState.Ready, is UsbExclusiveState.Active -> + usbExclusiveModeController.activeSession + else -> null + } + engine.setUsbExclusiveMode( + session, + usbExclusiveModeController::onSinkFormatChanged, + usbExclusiveModeController::onSessionDead + ) + + // Re-attach of a remembered device after an unplug: offer to resume by + // actually resuming, mirroring the headset-reconnect behavior. + if (state is UsbExclusiveState.Ready && usbResumeAfterReattach) { + usbResumeAfterReattach = false + val remembered = userPreferencesRepository.usbRememberedDevicesFlow.first() + if (remembered[state.device.key]?.autoResume == true) { + mediaSession?.player?.play() + } + } + } + } + // DAC unplugged mid-playback: pause immediately; the state collector above swaps the + // engine back to the normal output path without losing the queue. + serviceScope.launch { + usbExclusiveModeController.sessionLost.collect { + val player = mediaSession?.player + usbResumeAfterReattach = player?.playWhenReady == true + player?.pause() + } + } + serviceScope.launch { userPreferencesRepository.resumeOnHeadsetReconnectFlow.collect { enabled -> resumeOnHeadsetReconnectEnabled = enabled @@ -528,10 +573,16 @@ class MusicService : MediaLibraryService() { } } - // ReplayGain preference collectors + // ReplayGain preference collectors. ReplayGain scales the player volume, which the + // bit-perfect USB path must never do — gate it on exclusive mode being inactive. serviceScope.launch { - userPreferencesRepository.replayGainEnabledFlow.collect { enabled -> - replayGainProcessor.setEnabled(enabled) + combine( + userPreferencesRepository.replayGainEnabledFlow, + usbExclusiveModeController.state + ) { enabled, usbState -> + enabled && usbState !is UsbExclusiveState.Ready && usbState !is UsbExclusiveState.Active + }.distinctUntilChanged().collect { effectiveEnabled -> + replayGainProcessor.setEnabled(effectiveEnabled) // Re-apply to current track when toggled replayGainProcessor.apply(mediaSession?.player?.currentMediaItem) } diff --git a/app/src/main/java/com/theveloper/pixelplay/data/service/player/DualPlayerEngine.kt b/app/src/main/java/com/theveloper/pixelplay/data/service/player/DualPlayerEngine.kt index c8f05dc0a..099ae8eaa 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/service/player/DualPlayerEngine.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/service/player/DualPlayerEngine.kt @@ -38,7 +38,11 @@ import androidx.media3.extractor.mp3.Mp3Extractor import androidx.media3.extractor.flac.FlacExtractor import com.theveloper.pixelplay.data.diagnostics.PerformanceMetrics import com.theveloper.pixelplay.data.model.TransitionSettings +import com.theveloper.pixelplay.data.service.player.usb.UsbAudioSink import com.theveloper.pixelplay.data.telegram.TelegramRepository +import com.theveloper.pixelplay.usbaudio.UsbAudioSession +import com.theveloper.pixelplay.usbaudio.negotiation.NegotiatedFormat +import com.theveloper.pixelplay.usbaudio.negotiation.SourceFormat import com.theveloper.pixelplay.utils.envelope import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineScope @@ -250,6 +254,15 @@ class DualPlayerEngine @Inject constructor( var hiFiModeEnabled: Boolean = false private set private var audioOffloadEnabled = !shouldDisableAudioOffloadByDefault() + + // USB exclusive (bit-perfect) output. When a session is set, buildAudioSink() routes + // decoded PCM to the USB driver instead of DefaultAudioSink; offload is forced off and + // crossfade transitions are suspended (see usbExclusiveActive consumers). + private var usbExclusiveSession: UsbAudioSession? = null + private var usbSinkFormatCallback: (NegotiatedFormat?, SourceFormat?) -> Unit = { _, _ -> } + private var usbSessionDeadCallback: () -> Unit = {} + private val _usbExclusiveActive = MutableStateFlow(false) + val usbExclusiveActive: StateFlow = _usbExclusiveActive.asStateFlow() private var transitionJob: Job? = null private var bufferingFallbackJob: Job? = null private var transitionRunning = false @@ -1044,6 +1057,12 @@ class DualPlayerEngine @Inject constructor( enableFloatOutput: Boolean, enableAudioOutputPlaybackParams: Boolean ): AudioSink { + usbExclusiveSession?.let { session -> + // Bit-perfect path: no processor chain, no AudioTrack. The sink itself + // reports float support, so the FFmpeg renderer decodes >16-bit + // material to float regardless of the Hi-Fi toggle. + return UsbAudioSink(session, usbSinkFormatCallback, usbSessionDeadCallback) + } return DefaultAudioSink.Builder(context) .setEnableFloatOutput(hiFiModeEnabled) .setEnableAudioOutputPlaybackParameters(enableAudioOutputPlaybackParams) @@ -1133,7 +1152,9 @@ class DualPlayerEngine @Inject constructor( setAudioAttributes(audioAttributes, false) val offloadPreferences = TrackSelectionParameters.AudioOffloadPreferences.Builder() .setAudioOffloadMode( - if (audioOffloadEnabled) { + // Offload hands the compressed stream to the DSP — incompatible with a + // custom sink, so it is forced off while USB exclusive mode is engaged. + if (audioOffloadEnabled && usbExclusiveSession == null) { TrackSelectionParameters.AudioOffloadPreferences.AUDIO_OFFLOAD_MODE_ENABLED } else { TrackSelectionParameters.AudioOffloadPreferences.AUDIO_OFFLOAD_MODE_DISABLED @@ -1193,6 +1214,27 @@ class DualPlayerEngine @Inject constructor( rebuildPlayersPreservingMasterState("Hi-Fi mode set to $enabled") } + /** + * Engages or releases USB exclusive (bit-perfect) output. Follows the [setHiFiMode] + * pattern: players are rebuilt preserving queue/position/params, so toggling mid-song + * resumes where it was — on the DAC, or back on the normal Android path. + */ + fun setUsbExclusiveMode( + session: UsbAudioSession?, + onFormatChanged: (NegotiatedFormat?, SourceFormat?) -> Unit = { _, _ -> }, + onSessionDead: () -> Unit = {} + ) { + if (usbExclusiveSession === session) return + usbExclusiveSession = session + usbSinkFormatCallback = onFormatChanged + usbSessionDeadCallback = onSessionDead + _usbExclusiveActive.value = session != null + if (!::playerA.isInitialized) return // first buildPlayer() will pick the session up + rebuildPlayersPreservingMasterState( + "USB exclusive mode ${if (session != null) "engaged" else "released"}" + ) + } + suspend fun resolveCloudUri(uri: Uri): Uri = withContext(Dispatchers.IO) { val uriString = uri.toString() resolvedUriCache.get(uriString)?.let { return@withContext it } @@ -1293,6 +1335,12 @@ class DualPlayerEngine @Inject constructor( } private suspend fun prepareNext(mediaItem: MediaItem, preferredAbsoluteIndex: Int, startPositionMs: Long = 0L) { + if (usbExclusiveSession != null) { + // Crossfade needs a second player fading on the same output — impossible (and + // volume-scaled, so not bit-perfect) while the DAC is exclusively claimed. + Timber.tag("TransitionDebug").d("USB exclusive active — skipping crossfade prepare") + return + } try { val snapshot = ensureQueueSnapshot() val currentAbsoluteIndex = resolveCurrentAbsoluteIndex(playerA.currentMediaItem ?: mediaItem, snapshot) diff --git a/app/src/main/java/com/theveloper/pixelplay/data/service/player/TransitionController.kt b/app/src/main/java/com/theveloper/pixelplay/data/service/player/TransitionController.kt index 38dfd726b..055e51a77 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/service/player/TransitionController.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/service/player/TransitionController.kt @@ -185,17 +185,26 @@ class TransitionController @Inject constructor( } } - combine(settingsFlow, isCrossfadeEnabledFlow) { resolution, isEnabled -> - Pair(resolution, isEnabled) + combine(settingsFlow, isCrossfadeEnabledFlow, engine.usbExclusiveActive) { resolution, isEnabled, usbExclusive -> + Triple(resolution, isEnabled, usbExclusive) }.distinctUntilChanged() // Crucial: prevents restarting the job if the same settings are emitted again - .collectLatest { (resolution, isEnabled) -> + .collectLatest { (resolution, isEnabled, usbExclusive) -> val settings = resolution.settings Timber.tag("TransitionDebug").d( - "Settings resolved: Mode=%s, Duration=%dms, GlobalEnabled=%s, Source=%s", - settings.mode, settings.durationMs, isEnabled, resolution.source + "Settings resolved: Mode=%s, Duration=%dms, GlobalEnabled=%s, Source=%s, UsbExclusive=%s", + settings.mode, settings.durationMs, isEnabled, resolution.source, usbExclusive ) + // Crossfade is a dual-player volume fade — neither possible (single exclusive + // output) nor bit-perfect while the USB DAC is claimed. Plain gapless advance. + if (usbExclusive) { + Timber.tag("TransitionDebug").d("USB exclusive active. Using default gap.") + engine.cancelNext() + engine.setPauseAtEndOfMediaItems(shouldPause = false) + return@collectLatest + } + val isGloballyDisabled = resolution.source == TransitionSource.GLOBAL_DEFAULT && !isEnabled // If globally disabled and we are using the global defaults, use no transition. diff --git a/app/src/main/java/com/theveloper/pixelplay/data/usb/UsbExclusiveModeController.kt b/app/src/main/java/com/theveloper/pixelplay/data/usb/UsbExclusiveModeController.kt new file mode 100644 index 000000000..6e143a4b4 --- /dev/null +++ b/app/src/main/java/com/theveloper/pixelplay/data/usb/UsbExclusiveModeController.kt @@ -0,0 +1,312 @@ +package com.theveloper.pixelplay.data.usb + +import android.hardware.usb.UsbDeviceConnection +import com.theveloper.pixelplay.data.preferences.UserPreferencesRepository +import com.theveloper.pixelplay.di.AppScope +import com.theveloper.pixelplay.usbaudio.UsbAudioSession +import com.theveloper.pixelplay.usbaudio.descriptor.ParseResult +import com.theveloper.pixelplay.usbaudio.descriptor.UacCapabilities +import com.theveloper.pixelplay.usbaudio.descriptor.UacCapabilityProber +import com.theveloper.pixelplay.usbaudio.descriptor.UsbControlTransfer +import com.theveloper.pixelplay.usbaudio.descriptor.UsbDescriptorParser +import com.theveloper.pixelplay.usbaudio.negotiation.Conversion +import com.theveloper.pixelplay.usbaudio.negotiation.NegotiatedFormat +import com.theveloper.pixelplay.usbaudio.negotiation.SourceFormat +import javax.inject.Inject +import javax.inject.Singleton +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import timber.log.Timber + +/** Everything the UI (and MusicService) needs to know about USB exclusive mode. */ +sealed interface UsbExclusiveState { + /** The preference is off — feature entirely dormant, zero cost. */ + data object Disabled : UsbExclusiveState + + /** Enabled, but no USB Audio Class device attached. */ + data object NoDevice : UsbExclusiveState + + /** A DAC is attached; permission not yet requested/granted. */ + data class DeviceDetected(val device: UsbDeviceInfo) : UsbExclusiveState + + /** System permission dialog in flight. */ + data class PermissionPending(val device: UsbDeviceInfo) : UsbExclusiveState + + /** The user said no; a retry can be offered. */ + data class PermissionDenied(val device: UsbDeviceInfo) : UsbExclusiveState + + /** Device opened, capabilities probed, session established — the engine can attach. */ + data class Ready(val device: UsbDeviceInfo, val capabilities: UacCapabilities) : UsbExclusiveState + + /** Audio is flowing to the DAC. */ + data class Active( + val device: UsbDeviceInfo, + val capabilities: UacCapabilities, + val format: NegotiatedFormat, + val source: SourceFormat, + val conversion: Conversion, + val hardwareVolume: Boolean + ) : UsbExclusiveState + + /** Something failed; [recoverable] states can be retried from the UI. */ + data class Error(val message: String, val recoverable: Boolean) : UsbExclusiveState +} + +/** + * Owns the USB exclusive mode state machine and the [UsbAudioSession] lifecycle: + * preference × attached devices × permission events in, session + [UsbExclusiveState] out. + * MusicService attaches/detaches the engine's sink in response to [state]; the settings and + * player UIs render it. + */ +@Singleton +class UsbExclusiveModeController @Inject constructor( + private val usbDeviceManager: UsbDeviceManager, + private val userPreferencesRepository: UserPreferencesRepository, + @AppScope private val scope: CoroutineScope, + private val sessionFactory: UsbSessionFactory +) { + private val _state = MutableStateFlow(UsbExclusiveState.Disabled) + val state: StateFlow = _state.asStateFlow() + + /** Fires when an engaged session is lost involuntarily (unplug) — the service pauses. */ + private val _sessionLost = MutableSharedFlow(extraBufferCapacity = 4) + val sessionLost: SharedFlow = _sessionLost.asSharedFlow() + + /** The established session while in Ready/Active; the engine builds its sink around it. */ + @Volatile + var activeSession: UsbAudioSession? = null + private set + + private val mutex = Mutex() + private var sessionDevice: UsbDeviceInfo? = null + private var deniedDeviceKeys = mutableSetOf() + private var requestedDeviceKeys = mutableSetOf() + + init { + scope.launch { + combine( + userPreferencesRepository.usbExclusiveModeEnabledFlow, + usbDeviceManager.attachedAudioDevices + ) { enabled, devices -> enabled to devices } + .collect { (enabled, devices) -> reconcile(enabled, devices) } + } + scope.launch { + usbDeviceManager.permissionEvents.collect { result -> onPermissionResult(result) } + } + } + + /** Called from the settings UI to retry after a denial. */ + fun retryPermission(device: UsbDeviceInfo) { + deniedDeviceKeys.remove(device.key) + requestedDeviceKeys.remove(device.key) + scope.launch { + reconcile( + userPreferencesRepository.usbExclusiveModeEnabledFlow.first(), + usbDeviceManager.attachedAudioDevices.value + ) + } + } + + /** Sink → controller: the negotiated stream format changed (or ended with null). */ + fun onSinkFormatChanged(format: NegotiatedFormat?, source: SourceFormat?) { + val device = sessionDevice ?: return + val session = activeSession ?: return + _state.value = if (format != null && source != null) { + UsbExclusiveState.Active( + device = device, + capabilities = session.capabilities, + format = format, + source = source, + conversion = format.conversion, + hardwareVolume = session.capabilities.volume != null + ) + } else { + UsbExclusiveState.Ready(device, session.capabilities) + } + } + + /** Sink → controller: writes started failing (device unplugged or driver died). */ + fun onSessionDead() { + scope.launch { + mutex.withLock { + val device = sessionDevice ?: return@launch + Timber.tag(TAG).w("Session died for %s", device.key) + closeSessionLocked(lost = true) + _state.value = UsbExclusiveState.NoDevice + } + usbDeviceManager.refreshAttachedDevices() + } + } + + /** Maps a 0..1 slider fraction onto the DAC's hardware volume range. */ + fun setHardwareVolume(fraction: Float): Boolean { + val session = activeSession ?: return false + val range = session.volumeRangeDb256() ?: return false + val (min, max, resolution) = Triple(range[0], range[1], range[2].coerceAtLeast(1)) + val target = min + ((max - min) * fraction.coerceIn(0f, 1f)).toInt() + val stepped = min + ((target - min) / resolution) * resolution + return session.setVolumeDb256(stepped) + } + + private suspend fun reconcile(enabled: Boolean, devices: List) { + mutex.withLock { + if (!enabled) { + closeSessionLocked(lost = false) + deniedDeviceKeys.clear() + requestedDeviceKeys.clear() + _state.value = UsbExclusiveState.Disabled + return + } + + // Keep the current session while its device stays attached. + val currentDevice = sessionDevice + if (currentDevice != null) { + val stillAttached = devices.any { it.deviceName == currentDevice.deviceName } + val stillAlive = activeSession?.isAlive != false + if (stillAttached && stillAlive) return + closeSessionLocked(lost = true) + } + + val device = devices.firstOrNull() + if (device == null) { + _state.value = UsbExclusiveState.NoDevice + return + } + + when { + device.hasPermission -> openSessionLocked(device) + device.key in deniedDeviceKeys -> + _state.value = UsbExclusiveState.PermissionDenied(device) + device.key in requestedDeviceKeys -> + _state.value = UsbExclusiveState.PermissionPending(device) + else -> { + // The user opted into exclusive mode, so ask right away on attach. + requestedDeviceKeys.add(device.key) + _state.value = UsbExclusiveState.PermissionPending(device) + usbDeviceManager.requestPermission(device) + } + } + } + } + + private suspend fun onPermissionResult(result: UsbPermissionResult) { + when (result) { + is UsbPermissionResult.Granted -> { + deniedDeviceKeys.remove(result.device.key) + userPreferencesRepository.rememberUsbDevice( + result.device.key, + com.theveloper.pixelplay.data.usb.UsbRememberedDevice(label = result.device.displayName) + ) + mutex.withLock { + if (sessionDevice == null && + userPreferencesRepository.usbExclusiveModeEnabledFlow.first() + ) { + openSessionLocked(result.device) + } + } + } + + is UsbPermissionResult.Denied -> { + deniedDeviceKeys.add(result.device.key) + if (sessionDevice == null) { + _state.value = UsbExclusiveState.PermissionDenied(result.device) + } + } + } + } + + private suspend fun openSessionLocked(device: UsbDeviceInfo) { + val opened = withContext(Dispatchers.IO) { + val connection = usbDeviceManager.openConnection(device) + ?: return@withContext OpenOutcome.Failed("Could not open ${device.displayName}", true) + + val raw = connection.rawDescriptors + ?: return@withContext OpenOutcome.Failed("No descriptors from ${device.displayName}", false) + .also { connection.close() } + + val topology = when (val parsed = UsbDescriptorParser.parse(raw)) { + is ParseResult.Failure -> { + connection.close() + return@withContext OpenOutcome.Failed( + "${device.displayName}: ${parsed.reason}", false + ) + } + is ParseResult.Success -> parsed.topology + } + + val controlTransfer = UsbControlTransfer { requestType, request, value, index, buffer -> + connection.controlTransfer(requestType, request, value, index, buffer, buffer.size, 1000) + } + val capabilities = UacCapabilityProber.probe(topology, controlTransfer) + if (capabilities.formats.isEmpty()) { + connection.close() + return@withContext OpenOutcome.Failed( + "${device.displayName} reports no playable formats", false + ) + } + + val session = sessionFactory.open(connection, capabilities) + ?: return@withContext OpenOutcome.Failed( + "Driver could not attach to ${device.displayName}", true + ).also { connection.close() } + + OpenOutcome.Opened(session, capabilities) + } + + when (opened) { + is OpenOutcome.Opened -> { + activeSession = opened.session + sessionDevice = device + _state.value = UsbExclusiveState.Ready(device, opened.capabilities) + Timber.tag(TAG).i( + "USB exclusive ready: %s (%s, rates=%s)", + device.displayName, opened.capabilities.version, + opened.capabilities.allSampleRatesHz + ) + } + + is OpenOutcome.Failed -> { + Timber.tag(TAG).w("USB exclusive open failed: %s", opened.message) + _state.value = UsbExclusiveState.Error(opened.message, opened.recoverable) + } + } + } + + private suspend fun closeSessionLocked(lost: Boolean) { + val session = activeSession ?: return + val device = sessionDevice + activeSession = null + sessionDevice = null + withContext(Dispatchers.IO) { runCatching { session.close() } } + if (lost && device != null) { + _sessionLost.tryEmit(device) + } + Timber.tag(TAG).i("USB session closed (lost=%b)", lost) + } + + private sealed interface OpenOutcome { + data class Opened(val session: UsbAudioSession, val capabilities: UacCapabilities) : OpenOutcome + data class Failed(val message: String, val recoverable: Boolean) : OpenOutcome + } + + private companion object { + private const val TAG = "UsbExclusiveMode" + } +} + +/** Indirection over [UsbAudioSession.open] so the state machine is unit-testable. */ +fun interface UsbSessionFactory { + fun open(connection: UsbDeviceConnection, capabilities: UacCapabilities): UsbAudioSession? +} diff --git a/app/src/main/java/com/theveloper/pixelplay/di/AppModule.kt b/app/src/main/java/com/theveloper/pixelplay/di/AppModule.kt index 7f1d93599..48adb9ce2 100644 --- a/app/src/main/java/com/theveloper/pixelplay/di/AppModule.kt +++ b/app/src/main/java/com/theveloper/pixelplay/di/AppModule.kt @@ -113,6 +113,14 @@ object AppModule { return CoroutineScope(SupervisorJob() + Dispatchers.IO) } + @Singleton + @Provides + fun provideUsbSessionFactory(): com.theveloper.pixelplay.data.usb.UsbSessionFactory { + return com.theveloper.pixelplay.data.usb.UsbSessionFactory { connection, capabilities -> + com.theveloper.pixelplay.usbaudio.UsbAudioSession.open(connection, capabilities) + } + } + @Singleton @Provides fun provideWorkManager(@ApplicationContext context: Context): WorkManager { diff --git a/app/src/test/java/com/theveloper/pixelplay/data/usb/UsbExclusiveModeControllerTest.kt b/app/src/test/java/com/theveloper/pixelplay/data/usb/UsbExclusiveModeControllerTest.kt new file mode 100644 index 000000000..9a9c4885a --- /dev/null +++ b/app/src/test/java/com/theveloper/pixelplay/data/usb/UsbExclusiveModeControllerTest.kt @@ -0,0 +1,252 @@ +package com.theveloper.pixelplay.data.usb + +import android.hardware.usb.UsbDeviceConnection +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import com.theveloper.pixelplay.data.preferences.UserPreferencesRepository +import com.theveloper.pixelplay.usbaudio.UsbAudioSession +import com.theveloper.pixelplay.usbaudio.descriptor.UacCapabilities +import com.theveloper.pixelplay.usbaudio.descriptor.UacVersion +import io.mockk.every +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test + +class UsbExclusiveModeControllerTest { + + private val enabledFlow = MutableStateFlow(false) + private val attachedFlow = MutableStateFlow>(emptyList()) + private val permissionFlow = MutableSharedFlow(extraBufferCapacity = 4) + private val rememberedFlow = MutableStateFlow>(emptyMap()) + + private val requestedPermissions = mutableListOf() + private var sessionsOpened = 0 + private var sessionCloseCount = 0 + + private val deviceManager: UsbDeviceManager = mockk(relaxed = true) { + every { attachedAudioDevices } returns attachedFlow + every { permissionEvents } returns permissionFlow + every { requestPermission(any()) } answers { requestedPermissions += firstArg() } + } + + private val prefs: UserPreferencesRepository = mockk(relaxed = true) { + every { usbExclusiveModeEnabledFlow } returns enabledFlow + every { usbRememberedDevicesFlow } returns rememberedFlow + coEvery { rememberUsbDevice(any(), any()) } answers { + rememberedFlow.value = rememberedFlow.value + + (firstArg() to secondArg()) + } + } + + private val session: UsbAudioSession = mockk(relaxed = true) { + every { isAlive } returns true + every { capabilities } returns caps() + every { close() } answers { sessionCloseCount++ } + } + + private fun caps() = UacCapabilities( + version = UacVersion.UAC1, + controlInterfaceNumber = 0, + formats = emptyList(), // not consulted by the controller itself + volume = null + ) + + private lateinit var scope: CoroutineScope + + private fun device(permission: Boolean, name: String = "/dev/bus/usb/001/002") = UsbDeviceInfo( + deviceName = name, + productName = "Test DAC", + manufacturerName = "ACME", + vendorId = 0x1234, + productId = 0x5678, + serialNumber = if (permission) "S1" else null, + hasPermission = permission + ) + + /** Minimal but valid UAC1 device: header, terminals, one 44.1/48 kHz alt setting. */ + private fun uac1Blob(): ByteArray { + fun d(type: Int, vararg payload: Int): List = + (listOf(payload.size + 2, type) + payload.toList()).map { it.toByte() } + val bytes = mutableListOf() + bytes += d(0x01, 0, 2, 0, 0, 0, 64, 0x34, 0x12, 0x78, 0x56, 0, 1, 0, 0, 0, 1) + bytes += d(0x02, 0, 0, 2, 1, 0, 0xC0, 50) + bytes += d(0x04, 0, 0, 0, 1, 1, 0, 0) + bytes += d(0x24, 0x01, 0x00, 0x01, 40, 0, 1, 1) + bytes += d(0x24, 0x02, 0x01, 0x01, 0x01, 0, 2, 3, 0, 0, 0) + bytes += d(0x24, 0x03, 0x03, 0x02, 0x03, 0, 0x01, 0) + bytes += d(0x04, 1, 0, 0, 1, 2, 0, 0) + bytes += d(0x04, 1, 1, 1, 1, 2, 0, 0) + bytes += d(0x24, 0x01, 0x01, 1, 0x01, 0x00) + bytes += d(0x24, 0x02, 0x01, 2, 2, 16, 2, 0x44, 0xAC, 0, 0x80, 0xBB, 0) + bytes += d(0x05, 0x01, 0x09, 192, 0, 1, 0, 0) + bytes += d(0x25, 0x01, 0x01, 0, 0, 0) + return bytes.toByteArray() + } + + private fun controller(openable: Boolean = true): UsbExclusiveModeController { + val connection: UsbDeviceConnection = mockk(relaxed = true) { + every { rawDescriptors } returns uac1Blob() + } + every { deviceManager.openConnection(any()) } returns if (openable) connection else null + scope = CoroutineScope(UnconfinedTestDispatcher()) + return UsbExclusiveModeController( + usbDeviceManager = deviceManager, + userPreferencesRepository = prefs, + scope = scope, + sessionFactory = { _, _ -> sessionsOpened++; session } + ) + } + + @AfterEach + fun tearDown() { + scope.cancel() + } + + @Test + fun `disabled stays disabled regardless of devices`() = runTest { + val controller = controller() + controller.state.test { + assertThat(awaitItem()).isEqualTo(UsbExclusiveState.Disabled) + attachedFlow.value = listOf(device(permission = true)) + expectNoEvents() + } + assertThat(sessionsOpened).isEqualTo(0) + } + + @Test + fun `enabled with no device reports NoDevice`() = runTest { + val controller = controller() + controller.state.test { + assertThat(awaitItem()).isEqualTo(UsbExclusiveState.Disabled) + enabledFlow.value = true + assertThat(awaitItem()).isEqualTo(UsbExclusiveState.NoDevice) + } + } + + @Test + fun `attach without permission requests it and opens on grant`() = runTest { + val controller = controller() + controller.state.test { + assertThat(awaitItem()).isEqualTo(UsbExclusiveState.Disabled) + enabledFlow.value = true + assertThat(awaitItem()).isEqualTo(UsbExclusiveState.NoDevice) + + val dac = device(permission = false) + attachedFlow.value = listOf(dac) + assertThat(awaitItem()).isInstanceOf(UsbExclusiveState.PermissionPending::class.java) + assertThat(requestedPermissions).hasSize(1) + + val granted = device(permission = true) + attachedFlow.value = listOf(granted) + permissionFlow.tryEmit(UsbPermissionResult.Granted(granted)) + + val ready = awaitItem() + assertThat(ready).isInstanceOf(UsbExclusiveState.Ready::class.java) + assertThat(sessionsOpened).isEqualTo(1) + assertThat(controller.activeSession).isNotNull() + } + coVerify { prefs.rememberUsbDevice(any(), any()) } + } + + @Test + fun `denial is remembered and surfaced`() = runTest { + val controller = controller() + controller.state.test { + assertThat(awaitItem()).isEqualTo(UsbExclusiveState.Disabled) + enabledFlow.value = true + assertThat(awaitItem()).isEqualTo(UsbExclusiveState.NoDevice) + + val dac = device(permission = false) + attachedFlow.value = listOf(dac) + assertThat(awaitItem()).isInstanceOf(UsbExclusiveState.PermissionPending::class.java) + + permissionFlow.tryEmit(UsbPermissionResult.Denied(dac)) + assertThat(awaitItem()).isInstanceOf(UsbExclusiveState.PermissionDenied::class.java) + assertThat(sessionsOpened).isEqualTo(0) + } + } + + @Test + fun `detach while ready closes the session exactly once and reports loss`() = runTest { + val controller = controller() + controller.sessionLost.test { + enabledFlow.value = true + attachedFlow.value = listOf(device(permission = true)) + // Wait until the session opened. + while (controller.activeSession == null) kotlinx.coroutines.yield() + + attachedFlow.value = emptyList() + assertThat(awaitItem().vendorId).isEqualTo(0x1234) + } + assertThat(sessionCloseCount).isEqualTo(1) + assertThat(controller.activeSession).isNull() + assertThat(controller.state.value).isEqualTo(UsbExclusiveState.NoDevice) + } + + @Test + fun `disable while ready closes the session without loss event`() = runTest { + val controller = controller() + enabledFlow.value = true + attachedFlow.value = listOf(device(permission = true)) + while (controller.activeSession == null) kotlinx.coroutines.yield() + + controller.sessionLost.test { + enabledFlow.value = false + expectNoEvents() + } + assertThat(sessionCloseCount).isEqualTo(1) + assertThat(controller.state.value).isEqualTo(UsbExclusiveState.Disabled) + } + + @Test + fun `unopenable device reports a recoverable error`() = runTest { + val controller = controller(openable = false) + controller.state.test { + assertThat(awaitItem()).isEqualTo(UsbExclusiveState.Disabled) + enabledFlow.value = true + assertThat(awaitItem()).isEqualTo(UsbExclusiveState.NoDevice) + attachedFlow.value = listOf(device(permission = true)) + val error = awaitItem() + assertThat(error).isInstanceOf(UsbExclusiveState.Error::class.java) + assertThat((error as UsbExclusiveState.Error).recoverable).isTrue() + } + } + + @Test + fun `sink format callbacks move between Ready and Active`() = runTest { + val controller = controller() + enabledFlow.value = true + attachedFlow.value = listOf(device(permission = true)) + while (controller.activeSession == null) kotlinx.coroutines.yield() + + val format = com.theveloper.pixelplay.usbaudio.negotiation.NegotiatedFormat( + candidate = com.theveloper.pixelplay.usbaudio.descriptor.FormatCandidate( + interfaceNumber = 1, altSetting = 1, channels = 2, subslotBytes = 2, + bitResolution = 16, sampleRatesHz = listOf(44_100), endpointAddress = 1, + maxPacketSize = 192, intervalCode = 1, + syncType = com.theveloper.pixelplay.usbaudio.descriptor.EndpointSyncType.ADAPTIVE, + feedbackEndpointAddress = null, clockSourceId = null, uac1SampleRateControl = true + ), + sampleRateHz = 44_100, + conversion = com.theveloper.pixelplay.usbaudio.negotiation.Conversion.BIT_PERFECT + ) + val source = com.theveloper.pixelplay.usbaudio.negotiation.SourceFormat(44_100, 16, 2, false) + + controller.onSinkFormatChanged(format, source) + val active = controller.state.value + assertThat(active).isInstanceOf(UsbExclusiveState.Active::class.java) + assertThat((active as UsbExclusiveState.Active).conversion.isBitPerfect).isTrue() + + controller.onSinkFormatChanged(null, null) + assertThat(controller.state.value).isInstanceOf(UsbExclusiveState.Ready::class.java) + } +} From 12a44f64e316c044bc8fb98dd63f091e6d5694b6 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Jul 2026 06:57:17 +0000 Subject: [PATCH 07/17] feat(usb): USB Audio settings screen and Now Playing output badge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - UsbAudioSettingsScreen (route usb_audio_settings, entry row in Playback settings next to the Hi-Fi toggle): exclusive-mode master toggle, live state card (detection/permission/ready/active/error with permission retry), connected-device card with UAC version and the probed sample rates/bit depths, per-device auto-resume switch, hardware-volume slider when the DAC exposes a feature unit or a fixed line-level warning with one-time acknowledgement otherwise, and a debug-only 440 Hz test tone that drives the raw driver directly (bring-up aid, bypasses ExoPlayer) - Now Playing file-info chip shows the actual DAC output while exclusive mode is active — "USB • 24bit/96kHz", with source→output rate shown when the DAC forced a conversion — plumbed through FullPlayerSlice like the existing route/bluetooth labels - Strings in en/es/fr Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FZMSPh3TgbRAMqgW4q1p6V --- .../components/player/FullPlayerContent.kt | 17 +- .../presentation/navigation/AppNavigation.kt | 9 + .../presentation/navigation/Screen.kt | 1 + .../screens/SettingsCategoryScreen.kt | 8 + .../screens/UsbAudioSettingsScreen.kt | 423 ++++++++++++++++++ .../presentation/viewmodel/PlayerViewModel.kt | 39 +- .../viewmodel/UsbAudioSettingsViewModel.kt | 133 ++++++ .../main/res/values-es/strings_settings.xml | 27 ++ .../main/res/values-fr/strings_settings.xml | 27 ++ app/src/main/res/values/strings_settings.xml | 27 ++ 10 files changed, 699 insertions(+), 12 deletions(-) create mode 100644 app/src/main/java/com/theveloper/pixelplay/presentation/screens/UsbAudioSettingsScreen.kt create mode 100644 app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/UsbAudioSettingsViewModel.kt diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/player/FullPlayerContent.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/player/FullPlayerContent.kt index 8a30f6f41..b749c3576 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/player/FullPlayerContent.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/player/FullPlayerContent.kt @@ -260,6 +260,7 @@ fun FullPlayerContent( val selectedRouteName = fullPlayerSlice.selectedRouteName val isBluetoothEnabled = fullPlayerSlice.isBluetoothEnabled val bluetoothName = fullPlayerSlice.bluetoothName + val usbOutputLabel = fullPlayerSlice.usbOutputLabel val navigationBarBottomInset = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() val queueGestureBottomExclusion = maxOf(20.dp, navigationBarBottomInset + 8.dp) val queueGestureBottomExclusionPx = with(LocalDensity.current) { @@ -531,6 +532,7 @@ fun FullPlayerContent( currentPositionProvider = currentPositionProvider, totalDurationValue = totalDurationValue, showPlayerFileInfo = showPlayerFileInfo, + usbOutputLabel = usbOutputLabel, onSeek = onSeek, expansionFractionProvider = expansionFractionProvider, isPlayingProvider = isPlayingProvider, @@ -1205,6 +1207,7 @@ private fun FullPlayerProgressSection( currentPositionProvider: () -> Long, totalDurationValue: Long, showPlayerFileInfo: Boolean, + usbOutputLabel: String?, onSeek: (Long) -> Unit, expansionFractionProvider: () -> Float, isPlayingProvider: () -> Boolean, @@ -1241,6 +1244,7 @@ private fun FullPlayerProgressSection( audioBitrate = audioBitrate, audioSampleRate = audioSampleRate, showAudioFileInfo = showPlayerFileInfo, + usbOutputLabel = usbOutputLabel, onSeek = onSeek, expansionFractionProvider = expansionFractionProvider, isPlayingProvider = isPlayingProvider, @@ -1638,6 +1642,7 @@ private fun PlayerProgressBarSection( audioBitrate: Int?, audioSampleRate: Int?, showAudioFileInfo: Boolean, + usbOutputLabel: String? = null, onSeek: (Long) -> Unit, expansionFractionProvider: () -> Float, isPlayingProvider: () -> Boolean, @@ -1672,15 +1677,17 @@ private fun PlayerProgressBarSection( kotlin.math.abs(reportedDuration - hintDuration) <= 1500L -> reportedDuration else -> minOf(reportedDuration, hintDuration) } - val audioMetaLabel = remember(showAudioFileInfo, audioMimeType, audioBitrate, audioSampleRate) { - if (showAudioFileInfo) { - formatAudioMetaLabel( + val audioMetaLabel = remember(showAudioFileInfo, audioMimeType, audioBitrate, audioSampleRate, usbOutputLabel) { + when { + !showAudioFileInfo -> null + // Bit-perfect USB output: show the actual DAC output format, which is the + // more truthful label while exclusive mode drives the stream. + usbOutputLabel != null -> usbOutputLabel + else -> formatAudioMetaLabel( mimeType = audioMimeType, bitrate = audioBitrate, sampleRate = audioSampleRate ) - } else { - null } } var displayAudioMetaLabel by remember(songId) { mutableStateOf(null) } diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/navigation/AppNavigation.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/navigation/AppNavigation.kt index 6a99712be..482c6432f 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/navigation/AppNavigation.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/navigation/AppNavigation.kt @@ -458,6 +458,15 @@ fun AppNavigation( ) } } + composable( + Screen.UsbAudio.route, + ) { + ScreenWrapper(navController = navController, playerViewModel = playerViewModel, animatedVisibilityScope = this) { + com.theveloper.pixelplay.presentation.screens.UsbAudioSettingsScreen( + onNavigationIconClick = { navController.popBackStack() } + ) + } + } composable( Screen.NeteaseDashboard.route, ) { diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/navigation/Screen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/navigation/Screen.kt index 08a7cff5d..458f3aca9 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/navigation/Screen.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/navigation/Screen.kt @@ -50,6 +50,7 @@ sealed class Screen(val route: String) { object WordDelimiterConfig : Screen("word_delimiter_config") object Equalizer : Screen("equalizer") object DeviceCapabilities : Screen("device_capabilities") + object UsbAudio : Screen("usb_audio_settings") object NeteaseDashboard : Screen("netease_dashboard") object QqMusicDashboard : Screen("qqmusic_dashboard") object NavidromeDashboard : Screen("navidrome_dashboard") diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsCategoryScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsCategoryScreen.kt index dd1672dbc..3463cdf8d 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsCategoryScreen.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsCategoryScreen.kt @@ -86,6 +86,7 @@ import androidx.compose.material.icons.rounded.Science import androidx.compose.material.icons.rounded.Search import androidx.compose.material.icons.rounded.Timer import androidx.compose.material.icons.rounded.UnfoldMore +import androidx.compose.material.icons.rounded.Usb import androidx.compose.material3.AlertDialog import androidx.compose.material3.BottomAppBar import androidx.compose.material3.ButtonDefaults @@ -856,6 +857,13 @@ fun SettingsCategoryScreen( enabled = uiState.hiFiModeDeviceSupported, leadingIcon = { Icon(painterResource(R.drawable.outline_high_quality_24), null, tint = MaterialTheme.colorScheme.secondary) } ) + SettingsItem( + title = stringResource(R.string.settings_usb_audio_title), + subtitle = stringResource(R.string.settings_usb_audio_subtitle), + leadingIcon = { Icon(Icons.Rounded.Usb, null, tint = MaterialTheme.colorScheme.secondary) }, + trailingIcon = { Icon(Icons.Rounded.ChevronRight, null, tint = MaterialTheme.colorScheme.onSurfaceVariant) }, + onClick = { navController.navigateSafely(Screen.UsbAudio.route) } + ) SwitchSettingItem( title = stringResource(R.string.settings_persistent_shuffle_title), subtitle = stringResource(R.string.settings_persistent_shuffle_subtitle), diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/UsbAudioSettingsScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/UsbAudioSettingsScreen.kt new file mode 100644 index 000000000..5d841e86f --- /dev/null +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/UsbAudioSettingsScreen.kt @@ -0,0 +1,423 @@ +package com.theveloper.pixelplay.presentation.screens + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBars +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.GraphicEq +import androidx.compose.material.icons.rounded.Usb +import androidx.compose.material.icons.rounded.VolumeUp +import androidx.compose.material.icons.rounded.Warning +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Slider +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.theveloper.pixelplay.BuildConfig +import com.theveloper.pixelplay.R +import com.theveloper.pixelplay.data.usb.UsbDeviceInfo +import com.theveloper.pixelplay.data.usb.UsbExclusiveState +import com.theveloper.pixelplay.presentation.components.CollapsibleCommonTopBar +import com.theveloper.pixelplay.presentation.viewmodel.UsbAudioSettingsViewModel +import com.theveloper.pixelplay.usbaudio.descriptor.UacCapabilities +import com.theveloper.pixelplay.usbaudio.descriptor.UacVersion +import java.util.Locale +import kotlin.math.roundToInt +import kotlinx.coroutines.launch + +@Composable +fun UsbAudioSettingsScreen( + onNavigationIconClick: () -> Unit, + viewModel: UsbAudioSettingsViewModel = hiltViewModel() +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + val density = LocalDensity.current + val coroutineScope = rememberCoroutineScope() + val lazyListState = rememberLazyListState() + + val statusBarHeight = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + val minTopBarHeight = 64.dp + statusBarHeight + val maxTopBarHeight = 180.dp + val minTopBarHeightPx = with(density) { minTopBarHeight.toPx() } + val maxTopBarHeightPx = with(density) { maxTopBarHeight.toPx() } + + val topBarHeight = remember { Animatable(maxTopBarHeightPx) } + var collapseFraction by remember { mutableFloatStateOf(0f) } + + LaunchedEffect(topBarHeight.value) { + collapseFraction = + 1f - ((topBarHeight.value - minTopBarHeightPx) / (maxTopBarHeightPx - minTopBarHeightPx)).coerceIn(0f, 1f) + } + + val nestedScrollConnection = remember { + object : NestedScrollConnection { + override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { + val delta = available.y + val isScrollingDown = delta < 0 + if (!isScrollingDown && (lazyListState.firstVisibleItemIndex > 0 || lazyListState.firstVisibleItemScrollOffset > 0)) { + return Offset.Zero + } + val previousHeight = topBarHeight.value + val newHeight = (previousHeight + delta).coerceIn(minTopBarHeightPx, maxTopBarHeightPx) + val consumed = newHeight - previousHeight + if (consumed.roundToInt() != 0) { + coroutineScope.launch { topBarHeight.snapTo(newHeight) } + } + val canConsumeScroll = !(isScrollingDown && newHeight == minTopBarHeightPx) + return if (canConsumeScroll) Offset(0f, consumed) else Offset.Zero + } + } + } + + LaunchedEffect(lazyListState.isScrollInProgress) { + if (!lazyListState.isScrollInProgress) { + val shouldExpand = topBarHeight.value > (minTopBarHeightPx + maxTopBarHeightPx) / 2 + val canExpand = lazyListState.firstVisibleItemIndex == 0 && lazyListState.firstVisibleItemScrollOffset == 0 + val targetValue = if (shouldExpand && canExpand) maxTopBarHeightPx else minTopBarHeightPx + if (topBarHeight.value != targetValue) { + coroutineScope.launch { topBarHeight.animateTo(targetValue, spring(stiffness = Spring.StiffnessMedium)) } + } + } + } + + Box( + modifier = Modifier + .nestedScroll(nestedScrollConnection) + .fillMaxSize() + ) { + val currentTopBarHeightDp = with(density) { topBarHeight.value.toDp() } + + LazyColumn( + state = lazyListState, + contentPadding = PaddingValues(top = currentTopBarHeightDp + 8.dp, bottom = 120.dp), + modifier = Modifier.fillMaxSize() + ) { + item(key = "usb_toggle_section") { + SettingsSection( + title = stringResource(R.string.settings_usb_audio_title), + icon = { Icon(Icons.Rounded.Usb, null, tint = MaterialTheme.colorScheme.primary) } + ) { + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + SwitchSettingItem( + title = stringResource(R.string.settings_usb_exclusive_toggle_title), + subtitle = stringResource(R.string.settings_usb_exclusive_toggle_subtitle), + checked = uiState.enabled, + onCheckedChange = { viewModel.setEnabled(it) }, + leadingIcon = { Icon(Icons.Rounded.Usb, null, tint = MaterialTheme.colorScheme.secondary) } + ) + if (!uiState.enabled) { + Text( + text = stringResource(R.string.settings_usb_disabled_hint), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 16.dp) + ) + } + } + } + } + + if (uiState.enabled) { + item(key = "usb_status_section") { + UsbStatusCard( + state = uiState.state, + onRetryPermission = viewModel::retryPermission + ) + } + + val capabilities = (uiState.state as? UsbExclusiveState.Ready)?.capabilities + ?: (uiState.state as? UsbExclusiveState.Active)?.capabilities + val device = (uiState.state as? UsbExclusiveState.Ready)?.device + ?: (uiState.state as? UsbExclusiveState.Active)?.device + + if (capabilities != null && device != null) { + item(key = "usb_device_section") { + UsbDeviceCard( + device = device, + capabilities = capabilities, + autoResume = uiState.rememberedDevices[device.key]?.autoResume ?: true, + onAutoResumeChange = { viewModel.setAutoResume(device, it) } + ) + } + + item(key = "usb_volume_section") { + UsbVolumeSection( + capabilities = capabilities, + maxVolumeAcknowledged = uiState.maxVolumeAcknowledged, + onAcknowledge = viewModel::acknowledgeMaxVolume, + onVolumeChange = viewModel::setHardwareVolume + ) + } + + if (BuildConfig.DEBUG && uiState.state is UsbExclusiveState.Ready) { + item(key = "usb_debug_tone") { + OutlinedButton( + onClick = viewModel::playTestTone, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 8.dp) + ) { + Icon(Icons.Rounded.GraphicEq, null) + Spacer(Modifier.padding(horizontal = 4.dp)) + Text(stringResource(R.string.settings_usb_test_tone)) + } + } + } + } + } + } + + CollapsibleCommonTopBar( + title = stringResource(R.string.settings_usb_audio_title), + collapseFraction = collapseFraction, + headerHeight = currentTopBarHeightDp, + onBackClick = onNavigationIconClick + ) + } +} + +@Composable +private fun UsbStatusCard( + state: UsbExclusiveState, + onRetryPermission: (UsbDeviceInfo) -> Unit +) { + val (message, isError) = when (state) { + UsbExclusiveState.Disabled -> null to false + UsbExclusiveState.NoDevice -> stringResource(R.string.settings_usb_state_no_device) to false + is UsbExclusiveState.DeviceDetected -> + stringResource(R.string.settings_usb_state_detected, state.device.displayName) to false + is UsbExclusiveState.PermissionPending -> + stringResource(R.string.settings_usb_state_permission_pending) to false + is UsbExclusiveState.PermissionDenied -> + stringResource(R.string.settings_usb_state_permission_denied, state.device.displayName) to true + is UsbExclusiveState.Ready -> + stringResource(R.string.settings_usb_state_ready, state.device.displayName) to false + is UsbExclusiveState.Active -> + stringResource(R.string.settings_usb_state_active, state.device.displayName) to false + is UsbExclusiveState.Error -> state.message to true + } + if (message == null) return + + Surface( + color = if (isError) MaterialTheme.colorScheme.errorContainer + else MaterialTheme.colorScheme.surfaceContainer, + shape = RoundedCornerShape(16.dp), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp) + ) { + Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text( + text = message, + style = MaterialTheme.typography.bodyMedium, + color = if (isError) MaterialTheme.colorScheme.onErrorContainer + else MaterialTheme.colorScheme.onSurface + ) + if (state is UsbExclusiveState.Active) { + val conversionText = if (state.conversion.isBitPerfect) { + stringResource(R.string.settings_usb_conversion_bit_perfect) + } else { + stringResource(R.string.settings_usb_conversion_converted) + } + Text( + text = "${formatUsbFormat(state.format.candidate.bitResolution, state.format.sampleRateHz)} • $conversionText", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.primary + ) + } + if (state is UsbExclusiveState.PermissionDenied) { + OutlinedButton(onClick = { onRetryPermission(state.device) }) { + Text(stringResource(R.string.settings_usb_retry_permission)) + } + } + } + } +} + +@Composable +private fun UsbDeviceCard( + device: UsbDeviceInfo, + capabilities: UacCapabilities, + autoResume: Boolean, + onAutoResumeChange: (Boolean) -> Unit +) { + SettingsSection( + title = stringResource(R.string.settings_usb_device_section), + icon = { Icon(Icons.Rounded.Usb, null, tint = MaterialTheme.colorScheme.primary) } + ) { + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Surface( + color = MaterialTheme.colorScheme.surfaceContainer, + shape = RoundedCornerShape(16.dp), + modifier = Modifier.fillMaxWidth() + ) { + Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) { + Text( + text = device.displayName, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface + ) + device.manufacturerName?.let { + Text( + text = it, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + InfoRow( + label = stringResource(R.string.settings_usb_device_class), + value = if (capabilities.version == UacVersion.UAC2) "2.0" else "1.0" + ) + InfoRow( + label = stringResource(R.string.settings_usb_device_rates), + value = capabilities.allSampleRatesHz.joinToString(", ") { formatKhz(it) } + ) + InfoRow( + label = stringResource(R.string.settings_usb_device_depths), + value = capabilities.allBitResolutions.joinToString(", ") { "${it}-bit" } + ) + } + } + SwitchSettingItem( + title = stringResource(R.string.settings_usb_remember_title), + subtitle = stringResource(R.string.settings_usb_remember_subtitle), + checked = autoResume, + onCheckedChange = onAutoResumeChange, + leadingIcon = { Icon(Icons.Rounded.Usb, null, tint = MaterialTheme.colorScheme.secondary) } + ) + } + } +} + +@Composable +private fun UsbVolumeSection( + capabilities: UacCapabilities, + maxVolumeAcknowledged: Boolean, + onAcknowledge: () -> Unit, + onVolumeChange: (Float) -> Unit +) { + SettingsSection( + title = stringResource(R.string.settings_usb_volume_section), + icon = { Icon(Icons.Rounded.VolumeUp, null, tint = MaterialTheme.colorScheme.primary) } + ) { + if (capabilities.volume != null) { + var sliderValue by remember { mutableFloatStateOf(0.75f) } + Surface( + color = MaterialTheme.colorScheme.surfaceContainer, + shape = RoundedCornerShape(16.dp), + modifier = Modifier.fillMaxWidth() + ) { + Column(modifier = Modifier.padding(16.dp)) { + Text( + text = stringResource(R.string.settings_usb_hw_volume_title), + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.onSurface + ) + Slider( + value = sliderValue, + onValueChange = { sliderValue = it }, + onValueChangeFinished = { onVolumeChange(sliderValue) } + ) + } + } + } else { + Surface( + color = if (maxVolumeAcknowledged) MaterialTheme.colorScheme.surfaceContainer + else MaterialTheme.colorScheme.errorContainer, + shape = RoundedCornerShape(16.dp), + modifier = Modifier.fillMaxWidth() + ) { + Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + Icons.Rounded.Warning, + null, + tint = if (maxVolumeAcknowledged) MaterialTheme.colorScheme.onSurfaceVariant + else MaterialTheme.colorScheme.onErrorContainer + ) + Spacer(Modifier.padding(horizontal = 6.dp)) + Text( + text = stringResource(R.string.settings_usb_max_volume_warning_title), + style = MaterialTheme.typography.titleSmall, + color = if (maxVolumeAcknowledged) MaterialTheme.colorScheme.onSurface + else MaterialTheme.colorScheme.onErrorContainer + ) + } + Text( + text = stringResource(R.string.settings_usb_max_volume_warning_text), + style = MaterialTheme.typography.bodySmall, + color = if (maxVolumeAcknowledged) MaterialTheme.colorScheme.onSurfaceVariant + else MaterialTheme.colorScheme.onErrorContainer + ) + if (!maxVolumeAcknowledged) { + OutlinedButton(onClick = onAcknowledge) { + Text(stringResource(R.string.settings_usb_max_volume_ack)) + } + } + } + } + } + } +} + +@Composable +private fun InfoRow(label: String, value: String) { + Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.Top) { + Text( + text = label, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(end = 12.dp) + ) + Spacer(Modifier.weight(1f)) + Text( + text = value, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.weight(2f, fill = false) + ) + } +} + +private fun formatKhz(hz: Int): String = + if (hz % 1000 == 0) "${hz / 1000}" else String.format(Locale.US, "%.1f", hz / 1000.0) + +private fun formatUsbFormat(bits: Int, rateHz: Int): String = + "${bits}-bit / ${formatKhz(rateHz)} kHz" diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt index 4ddadbede..eebf0a59e 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt @@ -215,6 +215,7 @@ class PlayerViewModel @Inject constructor( val playlistSelectionStateHolder: PlaylistSelectionStateHolder, private val playbackDispatchStateHolder: PlaybackDispatchStateHolder, private val mediaControllerSyncStateHolder: MediaControllerSyncStateHolder, + private val usbExclusiveModeController: com.theveloper.pixelplay.data.usb.UsbExclusiveModeController, private val sessionToken: SessionToken, private val mediaControllerFactory: com.theveloper.pixelplay.data.media.MediaControllerFactory ) : ViewModel() { @@ -1237,9 +1238,29 @@ class PlayerViewModel @Inject constructor( val isRemotePlaybackActive: Boolean = false, val selectedRouteName: String? = null, val isBluetoothEnabled: Boolean = false, - val bluetoothName: String? = null + val bluetoothName: String? = null, + /** "USB • 24bit/96kHz" while bit-perfect USB output is active, else null. */ + val usbOutputLabel: String? = null ) + /** Compact, language-neutral output badge; conversion shown as source→output rate. */ + private fun usbOutputLabelOf(state: com.theveloper.pixelplay.data.usb.UsbExclusiveState): String? { + val active = state as? com.theveloper.pixelplay.data.usb.UsbExclusiveState.Active ?: return null + fun kHz(hz: Int): String = + if (hz % 1000 == 0) "${hz / 1000}kHz" + else String.format(java.util.Locale.US, "%.1fkHz", hz / 1000.0) + val bits = active.format.candidate.bitResolution + return if (active.conversion.isBitPerfect) { + "USB • ${bits}bit/${kHz(active.format.sampleRateHz)}" + } else { + "USB • ${bits}bit/${kHz(active.source.sampleRateHz)}→${kHz(active.format.sampleRateHz)}" + } + } + + private val usbOutputLabelFlow = usbExclusiveModeController.state + .map { usbOutputLabelOf(it) } + .distinctUntilChanged() + // Intermediate combine #1: 5 settings flows private val fullPlayerSlicePart1 = combine( currentSongArtists, @@ -1264,11 +1285,13 @@ class PlayerViewModel @Inject constructor( immersiveLyricsTimeout, isImmersiveTemporarilyDisabled, isRemotePlaybackActive, - combine(selectedRouteName, bluetoothSlice) { route, bt -> route to bt } + combine(selectedRouteName, bluetoothSlice, usbOutputLabelFlow) { route, bt, usb -> + Triple(route, bt, usb) + } ) { immersive: Boolean, immersiveTimeout: Long, immersiveDisabled: Boolean, - remotePb: Boolean, routeAndBt: Pair -> - val (routeName, bt) = routeAndBt - FullPlayerSlicePart2(immersive, immersiveTimeout, immersiveDisabled, remotePb, routeName, bt.enabled, bt.name) + remotePb: Boolean, routeBtUsb: Triple -> + val (routeName, bt, usbLabel) = routeBtUsb + FullPlayerSlicePart2(immersive, immersiveTimeout, immersiveDisabled, remotePb, routeName, bt.enabled, bt.name, usbLabel) } private data class FullPlayerSlicePart1( @@ -1286,7 +1309,8 @@ class PlayerViewModel @Inject constructor( val isRemotePlaybackActive: Boolean, val selectedRouteName: String?, val isBluetoothEnabled: Boolean, - val bluetoothName: String? + val bluetoothName: String?, + val usbOutputLabel: String? ) val fullPlayerSlice: StateFlow = combine( @@ -1305,7 +1329,8 @@ class PlayerViewModel @Inject constructor( isRemotePlaybackActive = p2.isRemotePlaybackActive, selectedRouteName = p2.selectedRouteName, isBluetoothEnabled = p2.isBluetoothEnabled, - bluetoothName = p2.bluetoothName + bluetoothName = p2.bluetoothName, + usbOutputLabel = p2.usbOutputLabel ) } .distinctUntilChanged() diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/UsbAudioSettingsViewModel.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/UsbAudioSettingsViewModel.kt new file mode 100644 index 000000000..77fd58d2a --- /dev/null +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/UsbAudioSettingsViewModel.kt @@ -0,0 +1,133 @@ +package com.theveloper.pixelplay.presentation.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.theveloper.pixelplay.data.preferences.UserPreferencesRepository +import com.theveloper.pixelplay.data.usb.UsbDeviceInfo +import com.theveloper.pixelplay.data.usb.UsbExclusiveModeController +import com.theveloper.pixelplay.data.usb.UsbExclusiveState +import com.theveloper.pixelplay.data.usb.UsbRememberedDevice +import com.theveloper.pixelplay.data.service.player.usb.PcmRepacker +import com.theveloper.pixelplay.usbaudio.negotiation.FormatNegotiator +import com.theveloper.pixelplay.usbaudio.negotiation.SourceFormat +import dagger.hilt.android.lifecycle.HiltViewModel +import java.nio.ByteBuffer +import java.nio.ByteOrder +import javax.inject.Inject +import kotlin.math.PI +import kotlin.math.sin +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import timber.log.Timber + +@HiltViewModel +class UsbAudioSettingsViewModel @Inject constructor( + private val controller: UsbExclusiveModeController, + private val userPreferencesRepository: UserPreferencesRepository +) : ViewModel() { + + data class UiState( + val enabled: Boolean = false, + val state: UsbExclusiveState = UsbExclusiveState.Disabled, + val rememberedDevices: Map = emptyMap(), + val maxVolumeAcknowledged: Boolean = false + ) + + val uiState: StateFlow = combine( + userPreferencesRepository.usbExclusiveModeEnabledFlow, + controller.state, + userPreferencesRepository.usbRememberedDevicesFlow, + userPreferencesRepository.usbExclusiveMaxVolumeAckFlow + ) { enabled, state, remembered, acknowledged -> + UiState(enabled, state, remembered, acknowledged) + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), UiState()) + + fun setEnabled(enabled: Boolean) { + viewModelScope.launch { userPreferencesRepository.setUsbExclusiveModeEnabled(enabled) } + } + + fun retryPermission(device: UsbDeviceInfo) = controller.retryPermission(device) + + fun setAutoResume(device: UsbDeviceInfo, autoResume: Boolean) { + viewModelScope.launch { + userPreferencesRepository.rememberUsbDevice( + device.key, + UsbRememberedDevice(label = device.displayName, autoResume = autoResume) + ) + } + } + + fun acknowledgeMaxVolume() { + viewModelScope.launch { userPreferencesRepository.setUsbExclusiveMaxVolumeAck(true) } + } + + fun setHardwareVolume(fraction: Float) { + viewModelScope.launch(Dispatchers.IO) { controller.setHardwareVolume(fraction) } + } + + // ─── Debug: raw driver bring-up tone (bypasses ExoPlayer entirely) ──────── + + private var toneJob: Job? = null + + /** Plays 2 s of 440 Hz straight through the session; only meaningful while Ready. */ + fun playTestTone() { + if (controller.state.value !is UsbExclusiveState.Ready) return + val session = controller.activeSession ?: return + if (toneJob?.isActive == true) return + + toneJob = viewModelScope.launch(Dispatchers.Default) { + val format = FormatNegotiator.negotiate( + SourceFormat(sampleRateHz = 48_000, bitDepth = 16, channels = 2, isFloat = false), + session.capabilities + ) ?: return@launch + if (!session.configure(format)) { + Timber.tag("UsbTestTone").w("configure failed: %s", session.lastError) + return@launch + } + session.resume() + + val candidate = format.candidate + val rate = format.sampleRateHz + val frameBytes = candidate.channels * candidate.subslotBytes + val chunkFrames = 4096 + val buffer = ByteBuffer.allocateDirect(chunkFrames * frameBytes).order(ByteOrder.LITTLE_ENDIAN) + + var frame = 0L + val totalFrames = rate.toLong() * 2 + while (frame < totalFrames && isActive) { + buffer.clear() + var inChunk = 0 + while (inChunk < chunkFrames && frame < totalFrames) { + val amplitude = sin(2.0 * PI * 440.0 * frame / rate) * 0.25 + val s32top = (amplitude * Int.MAX_VALUE).toInt() + repeat(candidate.channels) { + PcmRepacker.writeSubslot(buffer, s32top, candidate.subslotBytes) + } + inChunk++ + frame++ + } + buffer.flip() + while (buffer.hasRemaining() && isActive) { + val written = session.write(buffer, buffer.position(), buffer.remaining()) + if (written < 0) return@launch + if (written == 0) delay(10) else buffer.position(buffer.position() + written) + } + } + // Let the ring drain, then park the stream silently. + delay(300) + session.pause() + } + } + + override fun onCleared() { + toneJob?.cancel() + super.onCleared() + } +} diff --git a/app/src/main/res/values-es/strings_settings.xml b/app/src/main/res/values-es/strings_settings.xml index 79a7e6257..16d41d98d 100644 --- a/app/src/main/res/values-es/strings_settings.xml +++ b/app/src/main/res/values-es/strings_settings.xml @@ -646,4 +646,31 @@ Volumen Pausar cuando el volumen llegue a cero Pausar automáticamente la reproducción cuando el volumen sea 0 + + Audio USB + Salida bit-perfect a DACs USB + Modo exclusivo USB + Toma control exclusivo de un DAC USB y reproduce en el formato nativo de la pista, sin pasar por el mezclador de Android + Actívalo para reclamar un DAC USB cuando se conecte. La reproducción vuelve a la salida normal cuando no hay DAC en uso. + Ningún DAC USB conectado + %1$s detectado + Esperando el permiso USB… + Se denegó el acceso a %1$s + %1$s conectado • listo + Reproduciendo en %1$s + Bit-perfect + Convertido — el DAC no admite el formato de origen + Conceder acceso + Dispositivo conectado + Clase de audio USB + Frecuencias de muestreo + Profundidades de bits + Reanudar al reconectar + Reanuda la reproducción cuando este DAC vuelva a conectarse + Volumen + Volumen por hardware + Salida fija a nivel de línea + Este DAC no expone control de volumen por hardware, así que la salida va a escala completa. Controla el volumen en tu amplificador o auriculares antes de reproducir. + Entendido + Reproducir tono de prueba diff --git a/app/src/main/res/values-fr/strings_settings.xml b/app/src/main/res/values-fr/strings_settings.xml index 5932d3935..dd0a87155 100644 --- a/app/src/main/res/values-fr/strings_settings.xml +++ b/app/src/main/res/values-fr/strings_settings.xml @@ -642,4 +642,31 @@ Volume Mettre en pause quand le volume atteint zéro Mettre automatiquement en pause la lecture lorsque le volume est à 0 + + Audio USB + Sortie bit-perfect vers les DAC USB + Mode exclusif USB + Prend le contrôle exclusif d’un DAC USB et lit au format natif de la piste, sans passer par le mixeur Android + Activez pour réserver un DAC USB dès qu’il est branché. La lecture revient à la sortie normale quand aucun DAC n’est utilisé. + Aucun DAC USB connecté + %1$s détecté + En attente de l’autorisation USB… + Accès à %1$s refusé + %1$s connecté • prêt + Lecture vers %1$s + Bit-perfect + Converti — le DAC ne prend pas en charge le format source + Autoriser l’accès + Appareil connecté + Classe audio USB + Fréquences d’échantillonnage + Profondeurs de bits + Reprise à la reconnexion + Reprend la lecture quand ce DAC est rebranché + Volume + Volume matériel + Sortie fixe au niveau ligne + Ce DAC n’expose pas de contrôle de volume matériel : la sortie est à pleine échelle. Réglez le volume sur votre amplificateur ou votre casque avant la lecture. + Compris + Jouer une tonalité de test diff --git a/app/src/main/res/values/strings_settings.xml b/app/src/main/res/values/strings_settings.xml index ea3602815..8e6a549c7 100644 --- a/app/src/main/res/values/strings_settings.xml +++ b/app/src/main/res/values/strings_settings.xml @@ -646,4 +646,31 @@ Volume Pause when volume reaches zero Automatically pause playback when the volume is set to 0 + + USB Audio + Bit-perfect output to USB DACs + USB exclusive mode + Take exclusive control of a USB DAC and play at the track’s native format, bypassing the Android mixer + Enable to claim a USB DAC when one is attached. Playback falls back to normal output whenever no DAC is in use. + No USB DAC connected + %1$s detected + Waiting for USB permission… + Access to %1$s was denied + %1$s connected • ready + Playing to %1$s + Bit-perfect + Converted — the DAC does not support the source format + Grant access + Connected device + USB Audio Class + Sample rates + Bit depths + Auto-resume on reconnect + Resume playback when this DAC is plugged back in + Volume + Hardware volume + Fixed line-level output + This DAC exposes no hardware volume control, so output is at full scale. Control loudness on your amplifier or headphones before playing. + Got it + Play test tone From 27df4ff201f127e0fc16d519cfd1d75a585d5891 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Jul 2026 07:04:48 +0000 Subject: [PATCH 08/17] docs(usb): exclusive-mode architecture, policies and hardware test checklist Also updates PlayerViewModelTest for the new UsbExclusiveModeController constructor parameter. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FZMSPh3TgbRAMqgW4q1p6V --- .../viewmodel/PlayerViewModelTest.kt | 3 + docs/usb-exclusive-mode.md | 119 ++++++++++++++++++ 2 files changed, 122 insertions(+) create mode 100644 docs/usb-exclusive-mode.md diff --git a/app/src/test/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModelTest.kt b/app/src/test/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModelTest.kt index 7996811c6..814b27bcb 100644 --- a/app/src/test/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModelTest.kt +++ b/app/src/test/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModelTest.kt @@ -315,6 +315,9 @@ class PlayerViewModelTest { mockPlaylistSelectionStateHolder, playbackDispatchStateHolder, mediaControllerSyncStateHolder, + mockk(relaxed = true) { + every { state } returns MutableStateFlow(com.theveloper.pixelplay.data.usb.UsbExclusiveState.Disabled) + }, sessionToken, mockMediaControllerFactory ) diff --git a/docs/usb-exclusive-mode.md b/docs/usb-exclusive-mode.md new file mode 100644 index 000000000..1ee8f462d --- /dev/null +++ b/docs/usb-exclusive-mode.md @@ -0,0 +1,119 @@ +# USB Exclusive Mode (bit-perfect USB DAC output) + +Fully opt-in output mode: when enabled in **Settings → Playback → USB Audio** and a USB +DAC is attached, PixelPlay claims the DAC's USB Audio Class interface directly (libusb on +the file descriptor from `UsbDeviceConnection` — no root) and streams decoded PCM at the +track's native sample rate and bit depth, bypassing AudioTrack/AudioFlinger and its +resampling/mixing. With the toggle off, playback is byte-for-byte the pre-existing path. + +## Architecture + +``` +FFmpeg / MediaCodec decoders (ExoPlayer renderers) + │ PCM 16/24/32/float + ▼ +UsbAudioSink (Media3 AudioSink, app module) ← the ONE branch point: + │ FormatNegotiator picks alt-setting + rate DualPlayerEngine.buildAudioSink() + │ PcmRepacker left-justifies into the DAC's returns this instead of + ▼ subslot (lossless on the bit-perfect path) DefaultAudioSink while engaged +UsbAudioSession (:usbaudio Kotlin) + │ JNI (direct ByteBuffers, non-blocking ring backpressure) + ▼ +Native driver (:usbaudio C++ / libusb 1.0.30, shared .so, LGPL-2.1) + UacDevice — wrap fd, detach kernel driver, claim AC+AS, alt settings, + UAC1 endpoint / UAC2 clock-source sample-rate, feature-unit volume + IsoStream — isochronous OUT pipeline (8 transfers × 8 packets), Q16.16 + fractional packet sizing, explicit-feedback correction on async + endpoints, silence-fill on underrun, alt-0 on stop +``` + +Control plane (all unit-tested, no hardware needed): `UsbDescriptorParser` builds a +`UacTopology` from `getRawDescriptors()`; `UacCapabilityProber` resolves UAC2 rates via a +clock-source RANGE request on EP0; `FormatNegotiator` picks the alt setting/rate. The +`UsbExclusiveModeController` state machine (`Disabled → NoDevice → PermissionPending → +Ready → Active`, plus `PermissionDenied`/`Error`) owns the session lifecycle; MusicService +collects it and swaps the engine's sink using the same rebuild-preserving-state path as +the Hi-Fi toggle. + +## Bit-perfect policy (what we do and don't touch) + +- **Never** on any path: dithering, volume scaling (`UsbAudioSink.setVolume` is a no-op), + platform audio effects (no AudioTrack session exists — the equalizer cannot apply). +- **Bit-perfect** label: the only transformation is left-justified subslot packing — + 16→24/32-bit zero-padding, and float32→int (exact for material that was an integer of + ≤24 bits, which is everything the FFmpeg float path produces from FLAC/ALAC ≤24-bit). + Mono duplication into a stereo DAC also counts as bit-perfect (samples unchanged). +- **Converted** label (shown in the Now Playing badge as `source→output` and in the USB + Audio settings): the DAC lacks the source rate (resampled via Sonic in 16-bit), has + less depth (truncated, no dither), or fewer channels (5.1/7.1 Dolby downmix). +- Playback **speed/pitch is pinned to 1×** while exclusive (any tempo processing would + not be bit-perfect); crossfade and ReplayGain (both volume scaling) are suspended and + resume automatically when exclusive mode disengages. Track changes are plain gapless + advances. + +## Audio focus, calls and system sounds + +Focus handling is unchanged (the engine's manual `AudioFocusRequest`): calls and alarms +**pause playback**; on focus return, playback resumes if it was a transient loss. There is +deliberately **no ducking** — ducking is volume scaling. While the DAC is claimed, other +apps and system sounds cannot open it; ringtones/notifications play on the phone's own +output. The deck/preview player likewise stays on the phone output while a DAC is claimed. + +## Volume + +If the DAC exposes a UAC feature unit with a master volume, the USB Audio settings screen +offers a **hardware volume** slider (SET CUR on the feature unit, in the DAC's own 1/256 dB +steps — the PCM samples are untouched). Otherwise the output is fixed at full scale and the +screen shows a one-time **line-level warning**. The phone's volume keys do not affect the +DAC in exclusive mode (there is no AudioTrack); mapping volume keys to the feature unit is +a possible follow-up via a Media3 device-volume override. + +## Lifecycle & edge cases + +- **Unplug mid-playback**: the driver reports the dead stream and the DETACHED broadcast + fires; the service pauses, the session closes exactly once, and the engine rebuilds onto + the normal output preserving queue/position. No crash, no lost queue. +- **Re-attach**: granted devices are remembered (`vendorId:productId:serial` in DataStore); + permission is re-requested automatically and, if the device's *auto-resume* switch is on + and playback was interrupted by the unplug, playback resumes. +- **Sample-rate switches between tracks** (44.1 kHz → 96 kHz): the sink reprograms the + alt setting/clock during `configure()`; the stream starts in silence so the DAC's PLL + locks before audio flows (no pops). +- **Background/doze**: the existing `mediaPlayback` foreground service and + `WAKE_MODE_LOCAL` wake lock keep the USB stream alive with the screen off. +- **Media session/notification**: unchanged — the session wraps the engine's master + player, and rebuilds are already propagated by the player-swap listeners. + +## Known limitations (MVP) + +- Async endpoints with *implicit* feedback (no explicit feedback endpoint) are paced at + nominal rate; devices needing implicit feedback may drift on very long playback. +- DSD (DoP) is not implemented; `UacCapabilities.dsdSupport` is the hook. +- Only the first attached DAC is used when several are connected. +- Playback speed control is unavailable while exclusive mode is engaged. + +## Hardware verification checklist + +Everything above the JNI line is covered by JUnit tests (`:usbaudio:test`, +`:app:testDebugUnitTest` — parser blobs, negotiation table, repack golden vectors, +controller transition table, sink position/drain/backpressure). The following needs a +physical DAC (ideally one UAC2 async device with a rate display and one UAC1 dongle): + +1. Attach DAC → permission dialog → grant → settings screen shows rates/depths. +2. Debug build: "Play test tone" at each advertised rate (driver bring-up, no ExoPlayer). +3. Play FLAC 16/44.1, 24/96, 24/192, WAV 24-bit, MP3, Opus — Now Playing shows + `USB • …` and the DAC's own display shows the *source* rate (ground truth). +4. While playing, other apps cannot output to the DAC; system sounds stay on the phone. +5. Queue through 44.1 → 96 → 44.1 tracks: no pops at the switches. +6. Seek, notification controls, headset buttons all work; seekbar tracks correctly. +7. Unplug mid-song: playback pauses, app does not crash, normal output works. +8. Replug: permission re-grant (or remembered) → auto-resume if enabled. +9. Incoming call and alarm: playback pauses and resumes; no mixing into the DAC. +10. 1-hour screen-off playback: stream survives doze. +11. Hardware-volume DAC: slider changes loudness; fixed-volume DAC: warning shown. + +## Licensing + +libusb is vendored unmodified (see `usbaudio/src/main/cpp/external/libusb/VENDORED.md`) +and built as its own shared library, `libusb-1.0.so`, in keeping with LGPL-2.1. The +license text ships in the same directory (`COPYING`). From e4b429c5956ccfe89dd9fae507b0d8efed6b3bad Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Jul 2026 12:42:43 +0000 Subject: [PATCH 09/17] fix(usb): probe DAC capabilities after claiming interfaces; support clock selectors First hardware test (FiiO BTR17) failed with 'reports no playable formats'. Two root causes, both fixed: - Rate probing ran before the native claim: the UAC2 clock RANGE request went through the Java UsbDeviceConnection while the kernel audio driver still owned the AudioControl interface, and usbfs rejects interface-recipient control transfers in that state. The session is now created and claims AC+AS (detaching the kernel driver) first, and all capability probing goes through a new libusb-backed nativeControlTransferIn - XMOS-style dual-clock trees were not modeled: the BTR17's streaming terminal references a CLOCK_SELECTOR feeding separate 44.1k/48k family clock sources. The parser now reads CLOCK_SELECTOR and CLOCK_MULTIPLIER descriptors, clock paths resolve transitively with per-pin mapping, the prober unions rates across all reachable sources, and configure() routes the selector to the right pin before programming the frequency Also: probe/claim failures are now recoverable with a Retry button in the USB Audio screen, and the controller logs the parsed topology plus a hex dump of the raw descriptors when probing fails, so the next field report is diagnosable from logcat alone. 6 new tests: selector parsing/path resolution, rate union with per- source pin mapping, dead-clock fallback, claim-before-probe ordering, recoverable claim failure. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FZMSPh3TgbRAMqgW4q1p6V --- .../data/usb/UsbExclusiveModeController.kt | 62 +++++++++++++--- .../screens/UsbAudioSettingsScreen.kt | 11 ++- .../viewmodel/UsbAudioSettingsViewModel.kt | 3 + .../main/res/values-es/strings_settings.xml | 1 + .../main/res/values-fr/strings_settings.xml | 1 + app/src/main/res/values/strings_settings.xml | 1 + .../usb/UsbExclusiveModeControllerTest.kt | 23 ++++++ usbaudio/src/main/cpp/jni_bridge.cpp | 33 +++++++++ usbaudio/src/main/cpp/uac_device.cpp | 17 +++++ usbaudio/src/main/cpp/uac_device.h | 7 ++ .../pixelplay/usbaudio/UsbAudioNative.kt | 23 ++++++ .../pixelplay/usbaudio/UsbAudioSession.kt | 61 ++++++++++++++-- .../usbaudio/descriptor/UacCapabilities.kt | 23 +++++- .../descriptor/UacCapabilityProber.kt | 42 ++++++++--- .../usbaudio/descriptor/UacTopology.kt | 71 +++++++++++++++++-- .../descriptor/UsbDescriptorParser.kt | 30 ++++++++ .../usbaudio/descriptor/TestDescriptors.kt | 51 +++++++++++++ .../descriptor/UacCapabilityProberTest.kt | 53 ++++++++++++++ .../descriptor/UsbDescriptorParserTest.kt | 32 +++++++++ 19 files changed, 514 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/data/usb/UsbExclusiveModeController.kt b/app/src/main/java/com/theveloper/pixelplay/data/usb/UsbExclusiveModeController.kt index 6e143a4b4..2c5953e03 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/usb/UsbExclusiveModeController.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/usb/UsbExclusiveModeController.kt @@ -111,6 +111,12 @@ class UsbExclusiveModeController @Inject constructor( fun retryPermission(device: UsbDeviceInfo) { deniedDeviceKeys.remove(device.key) requestedDeviceKeys.remove(device.key) + refresh() + } + + /** Re-runs detection/reconciliation — the retry path for recoverable errors. */ + fun refresh() { + usbDeviceManager.refreshAttachedDevices() scope.launch { reconcile( userPreferencesRepository.usbExclusiveModeEnabledFlow.first(), @@ -233,11 +239,13 @@ class UsbExclusiveModeController @Inject constructor( ?: return@withContext OpenOutcome.Failed("Could not open ${device.displayName}", true) val raw = connection.rawDescriptors - ?: return@withContext OpenOutcome.Failed("No descriptors from ${device.displayName}", false) + ?: return@withContext OpenOutcome.Failed("No descriptors from ${device.displayName}", true) .also { connection.close() } val topology = when (val parsed = UsbDescriptorParser.parse(raw)) { is ParseResult.Failure -> { + Timber.tag(TAG).w("Descriptor parse failed for %s: %s", device.key, parsed.reason) + logRawDescriptors(raw) connection.close() return@withContext OpenOutcome.Failed( "${device.displayName}: ${parsed.reason}", false @@ -245,22 +253,48 @@ class UsbExclusiveModeController @Inject constructor( } is ParseResult.Success -> parsed.topology } + Timber.tag(TAG).i( + "Topology %s: %s, AC=%d, altSettings=%s, clockSources=%s, selectors=%s", + device.key, topology.version, topology.controlInterfaceNumber, + topology.playbackAltSettings.map { + "if${it.interfaceNumber}/alt${it.altSetting} ${it.bitResolution}bit×${it.channels}ch ${it.dataEndpoint.syncType}" + }, + topology.clockSources.map { it.id }, + topology.clockSelectors.map { "${it.id}(pins=${it.pinSourceIds})" } + ) + + // The session (and its claim) must exist BEFORE rate probing: usbfs rejects + // interface-recipient control transfers while the kernel audio driver still + // owns the interface, so probing goes through libusb after the detach+claim. + val session = sessionFactory.open(connection, UacCapabilityProber.preliminary(topology)) + ?: return@withContext OpenOutcome.Failed( + "Driver could not attach to ${device.displayName}", true + ).also { connection.close() } + + val asInterface = topology.playbackAltSettings.first().interfaceNumber + if (!session.claim(topology.controlInterfaceNumber, asInterface)) { + val reason = session.lastError ?: "claim failed" + session.close() + return@withContext OpenOutcome.Failed( + "Could not claim ${device.displayName}: $reason", true + ) + } val controlTransfer = UsbControlTransfer { requestType, request, value, index, buffer -> - connection.controlTransfer(requestType, request, value, index, buffer, buffer.size, 1000) + session.controlTransferIn(requestType, request, value, index, buffer) } val capabilities = UacCapabilityProber.probe(topology, controlTransfer) if (capabilities.formats.isEmpty()) { - connection.close() + Timber.tag(TAG).w( + "No formats resolved for %s (driver error: %s)", device.key, session.lastError + ) + logRawDescriptors(raw) + session.close() return@withContext OpenOutcome.Failed( - "${device.displayName} reports no playable formats", false + "Could not read supported formats from ${device.displayName}", true ) } - - val session = sessionFactory.open(connection, capabilities) - ?: return@withContext OpenOutcome.Failed( - "Driver could not attach to ${device.displayName}", true - ).also { connection.close() } + session.installCapabilities(capabilities) OpenOutcome.Opened(session, capabilities) } @@ -296,6 +330,16 @@ class UsbExclusiveModeController @Inject constructor( Timber.tag(TAG).i("USB session closed (lost=%b)", lost) } + /** Field-debugging aid: the raw descriptor blob, hex-dumped in logcat-sized lines. */ + private fun logRawDescriptors(raw: ByteArray) { + raw.toList().chunked(64).forEachIndexed { index, chunk -> + Timber.tag(TAG).d( + "descriptors[%03d]: %s", index * 64, + chunk.joinToString("") { "%02x".format(it) } + ) + } + } + private sealed interface OpenOutcome { data class Opened(val session: UsbAudioSession, val capabilities: UacCapabilities) : OpenOutcome data class Failed(val message: String, val recoverable: Boolean) : OpenOutcome diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/UsbAudioSettingsScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/UsbAudioSettingsScreen.kt index 5d841e86f..ff5ca945d 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/UsbAudioSettingsScreen.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/UsbAudioSettingsScreen.kt @@ -158,7 +158,8 @@ fun UsbAudioSettingsScreen( item(key = "usb_status_section") { UsbStatusCard( state = uiState.state, - onRetryPermission = viewModel::retryPermission + onRetryPermission = viewModel::retryPermission, + onRetry = viewModel::refresh ) } @@ -216,7 +217,8 @@ fun UsbAudioSettingsScreen( @Composable private fun UsbStatusCard( state: UsbExclusiveState, - onRetryPermission: (UsbDeviceInfo) -> Unit + onRetryPermission: (UsbDeviceInfo) -> Unit, + onRetry: () -> Unit ) { val (message, isError) = when (state) { UsbExclusiveState.Disabled -> null to false @@ -267,6 +269,11 @@ private fun UsbStatusCard( Text(stringResource(R.string.settings_usb_retry_permission)) } } + if (state is UsbExclusiveState.Error && state.recoverable) { + OutlinedButton(onClick = onRetry) { + Text(stringResource(R.string.settings_usb_retry)) + } + } } } } diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/UsbAudioSettingsViewModel.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/UsbAudioSettingsViewModel.kt index 77fd58d2a..b5c338f12 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/UsbAudioSettingsViewModel.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/UsbAudioSettingsViewModel.kt @@ -55,6 +55,9 @@ class UsbAudioSettingsViewModel @Inject constructor( fun retryPermission(device: UsbDeviceInfo) = controller.retryPermission(device) + /** Retry after a recoverable error (probe/claim failure). */ + fun refresh() = controller.refresh() + fun setAutoResume(device: UsbDeviceInfo, autoResume: Boolean) { viewModelScope.launch { userPreferencesRepository.rememberUsbDevice( diff --git a/app/src/main/res/values-es/strings_settings.xml b/app/src/main/res/values-es/strings_settings.xml index 16d41d98d..9413b079d 100644 --- a/app/src/main/res/values-es/strings_settings.xml +++ b/app/src/main/res/values-es/strings_settings.xml @@ -673,4 +673,5 @@ Este DAC no expone control de volumen por hardware, así que la salida va a escala completa. Controla el volumen en tu amplificador o auriculares antes de reproducir. Entendido Reproducir tono de prueba + Reintentar diff --git a/app/src/main/res/values-fr/strings_settings.xml b/app/src/main/res/values-fr/strings_settings.xml index dd0a87155..349045014 100644 --- a/app/src/main/res/values-fr/strings_settings.xml +++ b/app/src/main/res/values-fr/strings_settings.xml @@ -669,4 +669,5 @@ Ce DAC n’expose pas de contrôle de volume matériel : la sortie est à pleine échelle. Réglez le volume sur votre amplificateur ou votre casque avant la lecture. Compris Jouer une tonalité de test + Réessayer diff --git a/app/src/main/res/values/strings_settings.xml b/app/src/main/res/values/strings_settings.xml index 8e6a549c7..bccb69b31 100644 --- a/app/src/main/res/values/strings_settings.xml +++ b/app/src/main/res/values/strings_settings.xml @@ -673,4 +673,5 @@ This DAC exposes no hardware volume control, so output is at full scale. Control loudness on your amplifier or headphones before playing. Got it Play test tone + Retry diff --git a/app/src/test/java/com/theveloper/pixelplay/data/usb/UsbExclusiveModeControllerTest.kt b/app/src/test/java/com/theveloper/pixelplay/data/usb/UsbExclusiveModeControllerTest.kt index 9a9c4885a..48d9bfd49 100644 --- a/app/src/test/java/com/theveloper/pixelplay/data/usb/UsbExclusiveModeControllerTest.kt +++ b/app/src/test/java/com/theveloper/pixelplay/data/usb/UsbExclusiveModeControllerTest.kt @@ -47,9 +47,13 @@ class UsbExclusiveModeControllerTest { } } + private var claimResult = true + private var claimCount = 0 + private val session: UsbAudioSession = mockk(relaxed = true) { every { isAlive } returns true every { capabilities } returns caps() + every { claim(any(), any()) } answers { claimCount++; claimResult } every { close() } answers { sessionCloseCount++ } } @@ -152,11 +156,30 @@ class UsbExclusiveModeControllerTest { val ready = awaitItem() assertThat(ready).isInstanceOf(UsbExclusiveState.Ready::class.java) assertThat(sessionsOpened).isEqualTo(1) + assertThat(claimCount).isEqualTo(1) // kernel driver detached before probing assertThat(controller.activeSession).isNotNull() } coVerify { prefs.rememberUsbDevice(any(), any()) } } + @Test + fun `claim failure reports a recoverable error and closes the session once`() = runTest { + claimResult = false + val controller = controller() + controller.state.test { + assertThat(awaitItem()).isEqualTo(UsbExclusiveState.Disabled) + enabledFlow.value = true + assertThat(awaitItem()).isEqualTo(UsbExclusiveState.NoDevice) + + attachedFlow.value = listOf(device(permission = true)) + val error = awaitItem() + assertThat(error).isInstanceOf(UsbExclusiveState.Error::class.java) + assertThat((error as UsbExclusiveState.Error).recoverable).isTrue() + } + assertThat(sessionCloseCount).isEqualTo(1) + assertThat(controller.activeSession).isNull() + } + @Test fun `denial is remembered and surfaced`() = runTest { val controller = controller() diff --git a/usbaudio/src/main/cpp/jni_bridge.cpp b/usbaudio/src/main/cpp/jni_bridge.cpp index 524b81b16..3e29f07b8 100644 --- a/usbaudio/src/main/cpp/jni_bridge.cpp +++ b/usbaudio/src/main/cpp/jni_bridge.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include "iso_stream.h" #include "libusb.h" @@ -121,6 +122,38 @@ Java_com_theveloper_pixelplay_usbaudio_UsbAudioNative_nativeSetSampleRate( return ok ? 0 : -1; } +JNIEXPORT jint JNICALL +Java_com_theveloper_pixelplay_usbaudio_UsbAudioNative_nativeSetClockSelector( + JNIEnv*, jobject, jlong handle, jint selectorId, jint acInterface, jint pin) { + NativeSession* session = fromHandle(handle); + if (session == nullptr) return -1; + std::lock_guard lock(session->mutex); + return session->device->setClockSelector(selectorId, acInterface, pin) ? 0 : -1; +} + +JNIEXPORT jint JNICALL +Java_com_theveloper_pixelplay_usbaudio_UsbAudioNative_nativeControlTransferIn( + JNIEnv* env, jobject, jlong handle, jint requestType, jint request, jint value, jint index, + jbyteArray buffer) { + NativeSession* session = fromHandle(handle); + if (session == nullptr || buffer == nullptr) return -1; + const jsize length = env->GetArrayLength(buffer); + if (length <= 0 || length > 0xFFFF) return -1; + std::vector scratch(static_cast(length)); + int read; + { + std::lock_guard lock(session->mutex); + read = session->device->controlTransferIn( + static_cast(requestType), static_cast(request), + static_cast(value), static_cast(index), + scratch.data(), static_cast(length)); + } + if (read > 0) { + env->SetByteArrayRegion(buffer, 0, read, reinterpret_cast(scratch.data())); + } + return read; +} + JNIEXPORT jint JNICALL Java_com_theveloper_pixelplay_usbaudio_UsbAudioNative_nativeStart(JNIEnv*, jobject, jlong handle) { auto stream = streamOf(handle); diff --git a/usbaudio/src/main/cpp/uac_device.cpp b/usbaudio/src/main/cpp/uac_device.cpp index 2eb9bca7c..3e4da0c72 100644 --- a/usbaudio/src/main/cpp/uac_device.cpp +++ b/usbaudio/src/main/cpp/uac_device.cpp @@ -21,6 +21,7 @@ constexpr uint8_t kReqGetRes = 0x84; // Control selectors constexpr uint8_t kCsSamFreqControl = 0x01; // clock source (UAC2) / endpoint (UAC1) +constexpr uint8_t kCxClockSelectorControl = 0x01; // clock selector pin (UAC2 §A.17.2) constexpr uint8_t kFuMuteControl = 0x01; constexpr uint8_t kFuVolumeControl = 0x02; @@ -137,6 +138,22 @@ bool UacDevice::setSampleRateUac1(int endpointAddress, uint32_t rateHz) { return true; } +bool UacDevice::setClockSelector(int selectorId, int acInterface, int pin) { + const uint8_t data[1] = {static_cast(pin & 0xFF)}; + const uint16_t value = static_cast(kCxClockSelectorControl << 8); + const uint16_t index = static_cast((selectorId << 8) | (acInterface & 0xFF)); + if (!controlOut(kClassInterfaceOut, kReqCur, value, index, data, sizeof(data))) { + return false; + } + UA_LOGI("Clock selector %d set to pin %d", selectorId, pin); + return true; +} + +int UacDevice::controlTransferIn(uint8_t requestType, uint8_t request, uint16_t value, + uint16_t index, uint8_t* data, uint16_t length) { + return controlIn(requestType, request, value, index, data, length); +} + bool UacDevice::getVolumeRangeDb256(int uacVersion, int unitId, int acInterface, int32_t out[3]) { const uint16_t value = static_cast(kFuVolumeControl << 8); // channel 0 = master const uint16_t index = static_cast((unitId << 8) | (acInterface & 0xFF)); diff --git a/usbaudio/src/main/cpp/uac_device.h b/usbaudio/src/main/cpp/uac_device.h index 3d5a585c8..37de8100d 100644 --- a/usbaudio/src/main/cpp/uac_device.h +++ b/usbaudio/src/main/cpp/uac_device.h @@ -27,6 +27,13 @@ class UacDevice { bool setSampleRateUac2(int clockId, int acInterface, uint32_t rateHz); bool setSampleRateUac1(int endpointAddress, uint32_t rateHz); + /** UAC2 clock selector: route the clock tree through 1-based input [pin]. */ + bool setClockSelector(int selectorId, int acInterface, int pin); + + /** Generic class/standard IN control transfer for capability probing (post-claim). */ + int controlTransferIn(uint8_t requestType, uint8_t request, uint16_t value, uint16_t index, + uint8_t* data, uint16_t length); + /** Volume values are UAC-native: signed 1/256 dB steps. out = {min, max, res}. */ bool getVolumeRangeDb256(int uacVersion, int unitId, int acInterface, int32_t out[3]); bool setVolumeDb256(int uacVersion, int unitId, int acInterface, int32_t valueDb256); diff --git a/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/UsbAudioNative.kt b/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/UsbAudioNative.kt index 690bd6117..25efbf3b1 100644 --- a/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/UsbAudioNative.kt +++ b/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/UsbAudioNative.kt @@ -65,6 +65,29 @@ object UsbAudioNative { rateHz: Int ): Int + /** Routes a UAC2 clock selector to the 1-based input [pin]. */ + external fun nativeSetClockSelector( + handle: Long, + selectorId: Int, + acInterface: Int, + pin: Int + ): Int + + /** + * Class/standard IN control transfer through libusb (valid once the interfaces are + * claimed — the Java UsbDeviceConnection path is rejected by usbfs while the kernel + * audio driver owns the interface). Returns bytes read into [buffer] or a negative + * libusb error. + */ + external fun nativeControlTransferIn( + handle: Long, + requestType: Int, + request: Int, + value: Int, + index: Int, + buffer: ByteArray + ): Int + external fun nativeStart(handle: Long): Int external fun nativePause(handle: Long): Int external fun nativeResume(handle: Long): Int diff --git a/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/UsbAudioSession.kt b/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/UsbAudioSession.kt index 3f831f751..9e66dcc5c 100644 --- a/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/UsbAudioSession.kt +++ b/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/UsbAudioSession.kt @@ -20,9 +20,23 @@ import java.nio.ByteBuffer class UsbAudioSession private constructor( private val connection: UsbDeviceConnection, private val handle: Long, - val capabilities: UacCapabilities + capabilities: UacCapabilities ) : AutoCloseable { + /** + * Starts as the topology-derived preliminary capabilities (correct version/volume, + * empty formats) and is replaced via [installCapabilities] once post-claim rate + * probing completes. Never mutated after the session reaches the Ready state. + */ + @Volatile + var capabilities: UacCapabilities = capabilities + private set + + /** Installs the fully-probed capabilities; exclusive-mode controller only. */ + fun installCapabilities(capabilities: UacCapabilities) { + this.capabilities = capabilities + } + private val lock = Any() private var closed = false private var claimed = false @@ -36,9 +50,32 @@ class UsbAudioSession private constructor( get() = if (capabilities.version == UacVersion.UAC2) 2 else 1 /** - * Programs [format]: claims interfaces on first use, selects the alt setting, sets the - * sample rate and starts the iso pipeline. Returns false (with [lastError] populated) - * on any failure; the session stays usable for a retry with another format. + * Detaches the kernel audio driver and claims the AudioControl + AudioStreaming + * interfaces. Idempotent. Must happen before [controlTransferIn] probing — usbfs + * rejects interface-recipient control transfers while another driver owns the + * interface, which is exactly the state a freshly-attached DAC is in on Android. + */ + fun claim(acInterface: Int, asInterface: Int): Boolean { + synchronized(lock) { + if (closed) return false + if (claimed) return true + if (UsbAudioNative.nativeClaim(handle, acInterface, asInterface) != 0) return false + claimed = true + return true + } + } + + /** Class IN control transfer via libusb (post-claim); see [UsbAudioNative]. */ + fun controlTransferIn(requestType: Int, request: Int, value: Int, index: Int, buffer: ByteArray): Int { + if (closed) return -1 + return UsbAudioNative.nativeControlTransferIn(handle, requestType, request, value, index, buffer) + } + + /** + * Programs [format]: claims interfaces on first use, selects the alt setting, routes + * the clock selector (dual-clock XMOS designs), sets the sample rate and starts the + * iso pipeline. Returns false (with [lastError] populated) on any failure; the + * session stays usable for a retry with another format. */ fun configure(format: NegotiatedFormat, ringBufferMs: Int = DEFAULT_RING_BUFFER_MS): Boolean { synchronized(lock) { @@ -72,10 +109,24 @@ class UsbAudioSession private constructor( return false } + // Dual-clock (44.1k/48k family) devices need the selector routed to the clock + // source that carries the target rate before that source is programmed. + val clock = candidate.clockForRate(format.sampleRateHz) + val selectorId = candidate.clockSelectorId + if (selectorId != null && clock?.selectorPin != null) { + if (UsbAudioNative.nativeSetClockSelector( + handle, selectorId, capabilities.controlInterfaceNumber, clock.selectorPin + ) != 0 + ) { + // Some firmwares auto-route on SET CUR frequency; log-and-continue. + // (Native layer already logged the failure.) + } + } + if (UsbAudioNative.nativeSetSampleRate( handle = handle, uacVersion = uacVersionCode, - clockId = candidate.clockSourceId ?: 0, + clockId = clock?.clockSourceId ?: candidate.clockSourceId ?: 0, acInterface = capabilities.controlInterfaceNumber, endpointAddress = candidate.endpointAddress, rateHz = format.sampleRateHz diff --git a/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/descriptor/UacCapabilities.kt b/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/descriptor/UacCapabilities.kt index 3c26b8683..5d6c5734c 100644 --- a/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/descriptor/UacCapabilities.kt +++ b/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/descriptor/UacCapabilities.kt @@ -24,6 +24,14 @@ data class UacCapabilities( enum class DsdSupport { NONE, DOP } +/** Rates one specific clock source can produce, and the selector pin that routes to it. */ +data class ClockRates( + val clockSourceId: Int, + val ratesHz: List, + /** 1-based selector pin, when a clock selector sits between terminal and source. */ + val selectorPin: Int? +) + /** One playable configuration: an alt setting with its resolved rates. */ data class FormatCandidate( val interfaceNumber: Int, @@ -33,17 +41,26 @@ data class FormatCandidate( val subslotBytes: Int, /** Meaningful bits (16, 24 or 32). */ val bitResolution: Int, + /** Union of rates across all reachable clock sources (UAC1: from the descriptors). */ val sampleRatesHz: List, val endpointAddress: Int, val maxPacketSize: Int, val intervalCode: Int, val syncType: EndpointSyncType, val feedbackEndpointAddress: Int?, - /** UAC2 clock source to program for this stream; null on UAC1. */ + /** Primary UAC2 clock source (first on the path); null on UAC1. */ val clockSourceId: Int?, /** UAC1 endpoint supports SET_CUR sampling frequency. */ - val uac1SampleRateControl: Boolean -) + val uac1SampleRateControl: Boolean, + /** Per-clock-source rates for selector-equipped devices (XMOS dual-clock designs). */ + val clockSources: List = emptyList(), + /** The clock selector to program before setting the rate, when present. */ + val clockSelectorId: Int? = null +) { + /** The clock (and selector pin) that can produce [rateHz], preferring exact support. */ + fun clockForRate(rateHz: Int): ClockRates? = + clockSources.firstOrNull { rateHz in it.ratesHz } ?: clockSources.firstOrNull() +} data class VolumeCapability( val featureUnitId: Int, diff --git a/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/descriptor/UacCapabilityProber.kt b/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/descriptor/UacCapabilityProber.kt index dbce3f983..177d444b1 100644 --- a/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/descriptor/UacCapabilityProber.kt +++ b/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/descriptor/UacCapabilityProber.kt @@ -35,11 +35,35 @@ object UacCapabilityProber { /** bmRequestType: device-to-host | class | interface. */ private const val REQUEST_TYPE_AC_GET = 0xA1 + /** + * Topology-only capabilities: correct volume/version info with an empty format list. + * Used to construct the session before the (post-claim) rate probing has run. + */ + fun preliminary(topology: UacTopology): UacCapabilities = UacCapabilities( + version = topology.version, + controlInterfaceNumber = topology.controlInterfaceNumber, + formats = emptyList(), + volume = resolveVolume(topology) + ) + fun probe(topology: UacTopology, controlTransfer: UsbControlTransfer): UacCapabilities { val formats = topology.playbackAltSettings.mapNotNull { alt -> + var clockRates = emptyList() + var clockSelectorId: Int? = null val rates = when (topology.version) { UacVersion.UAC1 -> uac1Rates(alt) - UacVersion.UAC2 -> uac2Rates(topology, alt, controlTransfer) + UacVersion.UAC2 -> { + val path = topology.clockPathFor(alt) ?: return@mapNotNull null + clockSelectorId = path.selector?.id + clockRates = path.sources.mapNotNull { source -> + val sourceRates = uac2RatesForClock( + source.id, topology.controlInterfaceNumber, controlTransfer + ) + if (sourceRates.isEmpty()) null + else ClockRates(source.id, sourceRates, path.pinBySourceId[source.id]) + } + clockRates.flatMap { it.ratesHz }.distinct().sorted() + } } if (rates.isEmpty()) return@mapNotNull null FormatCandidate( @@ -54,8 +78,11 @@ object UacCapabilityProber { intervalCode = alt.dataEndpoint.intervalCode, syncType = alt.dataEndpoint.syncType, feedbackEndpointAddress = alt.feedbackEndpoint?.address, - clockSourceId = topology.clockSourceFor(alt)?.id, - uac1SampleRateControl = alt.uac1SampleRateControl + clockSourceId = clockRates.firstOrNull()?.clockSourceId + ?: topology.clockSourceFor(alt)?.id, + uac1SampleRateControl = alt.uac1SampleRateControl, + clockSources = clockRates, + clockSelectorId = clockSelectorId ) } @@ -84,13 +111,12 @@ object UacCapabilityProber { else -> emptyList() } - private fun uac2Rates( - topology: UacTopology, - alt: StreamingAltSetting, + private fun uac2RatesForClock( + clockSourceId: Int, + controlInterfaceNumber: Int, controlTransfer: UsbControlTransfer ): List { - val clock = topology.clockSourceFor(alt) ?: return emptyList() - val index = (clock.id shl 8) or topology.controlInterfaceNumber + val index = (clockSourceId shl 8) or controlInterfaceNumber val value = CS_SAM_FREQ_CONTROL shl 8 // First learn the subrange count, then fetch the full layout: diff --git a/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/descriptor/UacTopology.kt b/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/descriptor/UacTopology.kt index 2866344a5..e6b047257 100644 --- a/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/descriptor/UacTopology.kt +++ b/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/descriptor/UacTopology.kt @@ -59,6 +59,34 @@ data class ClockSource( val frequencyProgrammable: Boolean get() = controls and 0x03 == 0x03 } +/** + * UAC2 clock selector (CLOCK_SELECTOR descriptor). XMOS-style DACs typically route the + * streaming terminal's clock through one of these, feeding a 44.1 kHz-family and a + * 48 kHz-family clock source on separate pins. + */ +data class ClockSelector( + val id: Int, + /** Upstream entity IDs; pin numbers are 1-based indices into this list. */ + val pinSourceIds: List, + val controls: Int +) + +/** UAC2 clock multiplier (CLOCK_MULTIPLIER descriptor) — passed through when resolving. */ +data class ClockMultiplier( + val id: Int, + val sourceId: Int +) + +/** The clock entities reachable from a streaming alt setting's terminal. */ +data class ClockPath( + /** The selector encountered first on the path, if any (the one to program). */ + val selector: ClockSelector?, + /** All reachable clock sources, in pin order when behind a selector. */ + val sources: List, + /** For each source id: the 1-based selector pin that routes to it. */ + val pinBySourceId: Map +) + /** Feature unit with per-channel mute/volume capabilities (channel 0 = master). */ data class FeatureUnit( val id: Int, @@ -90,20 +118,55 @@ data class UacTopology( val controlInterfaceNumber: Int, val terminals: List, val clockSources: List, + val clockSelectors: List = emptyList(), + val clockMultipliers: List = emptyList(), val featureUnits: List, /** Playback alt settings (iso OUT), across all AudioStreaming interfaces. */ val playbackAltSettings: List ) { /** - * The clock source feeding [alt], resolved through its terminal link (UAC2 only). + * Resolves the clock entities feeding [alt] through its terminal link (UAC2 only), + * walking selectors and multipliers transitively. Cycle-guarded — malformed + * descriptors cannot loop it. */ - fun clockSourceFor(alt: StreamingAltSetting): ClockSource? { + fun clockPathFor(alt: StreamingAltSetting): ClockPath? { if (version != UacVersion.UAC2) return null val terminal = terminals.firstOrNull { it.id == alt.terminalLink } ?: return null - val clockId = terminal.clockSourceId ?: return null - return clockSources.firstOrNull { it.id == clockId } + val startId = terminal.clockSourceId ?: return null + + val visited = mutableSetOf() + var firstSelector: ClockSelector? = null + val sources = mutableListOf() + val pinBySourceId = mutableMapOf() + + fun visit(entityId: Int, pin: Int?) { + if (!visited.add(entityId)) return + clockSources.firstOrNull { it.id == entityId }?.let { source -> + sources += source + if (pin != null) pinBySourceId[source.id] = pin + return + } + clockSelectors.firstOrNull { it.id == entityId }?.let { selector -> + if (firstSelector == null) firstSelector = selector + selector.pinSourceIds.forEachIndexed { index, upstreamId -> + // Pins are 1-based; nested selectors inherit the outer pin. + visit(upstreamId, pin ?: (index + 1)) + } + return + } + clockMultipliers.firstOrNull { it.id == entityId }?.let { multiplier -> + visit(multiplier.sourceId, pin) + } + } + visit(startId, null) + + return if (sources.isEmpty()) null else ClockPath(firstSelector, sources, pinBySourceId) } + /** Convenience: the primary clock source feeding [alt] (first on the path). */ + fun clockSourceFor(alt: StreamingAltSetting): ClockSource? = + clockPathFor(alt)?.sources?.firstOrNull() + /** * Feature unit controlling the playback path of [alt]: the unit fed directly by the * stream's USB-streaming input terminal, or failing that any unit with a master volume. diff --git a/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/descriptor/UsbDescriptorParser.kt b/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/descriptor/UsbDescriptorParser.kt index 2b54d1253..df6f08b62 100644 --- a/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/descriptor/UsbDescriptorParser.kt +++ b/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/descriptor/UsbDescriptorParser.kt @@ -28,6 +28,8 @@ object UsbDescriptorParser { private const val AC_OUTPUT_TERMINAL = 0x03 private const val AC_FEATURE_UNIT = 0x06 private const val AC2_CLOCK_SOURCE = 0x0A + private const val AC2_CLOCK_SELECTOR = 0x0B + private const val AC2_CLOCK_MULTIPLIER = 0x0C // AS class-specific descriptor subtypes private const val AS_GENERAL = 0x01 @@ -48,6 +50,8 @@ object UsbDescriptorParser { var controlInterface: Int? = null val terminals = mutableListOf() val clockSources = mutableListOf() + val clockSelectors = mutableListOf() + val clockMultipliers = mutableListOf() val featureUnits = mutableListOf() val altSettings = mutableListOf() @@ -147,6 +151,30 @@ object UsbDescriptorParser { ) } } + + AC2_CLOCK_SELECTOR -> { + // bLength = 7 + p: bClockID, bNrInPins, baCSourceID[p], + // bmControls, iClockSelector + if (version == UacVersion.UAC2 && d.bytes.size >= 7) { + val pins = d.u8(4) + if (pins in 1..(d.bytes.size - 7)) { + clockSelectors += ClockSelector( + id = d.u8(3), + pinSourceIds = (0 until pins).map { d.u8(5 + it) }, + controls = d.u8(5 + pins) + ) + } + } + } + + AC2_CLOCK_MULTIPLIER -> { + if (version == UacVersion.UAC2 && d.bytes.size >= 7) { + clockMultipliers += ClockMultiplier( + id = d.u8(3), + sourceId = d.u8(4) + ) + } + } } } @@ -213,6 +241,8 @@ object UsbDescriptorParser { controlInterfaceNumber = acInterface, terminals = terminals, clockSources = clockSources, + clockSelectors = clockSelectors, + clockMultipliers = clockMultipliers, featureUnits = featureUnits, playbackAltSettings = altSettings ) diff --git a/usbaudio/src/test/java/com/theveloper/pixelplay/usbaudio/descriptor/TestDescriptors.kt b/usbaudio/src/test/java/com/theveloper/pixelplay/usbaudio/descriptor/TestDescriptors.kt index 2554f39e6..85251b250 100644 --- a/usbaudio/src/test/java/com/theveloper/pixelplay/usbaudio/descriptor/TestDescriptors.kt +++ b/usbaudio/src/test/java/com/theveloper/pixelplay/usbaudio/descriptor/TestDescriptors.kt @@ -117,6 +117,57 @@ internal fun uac2AsyncDacDescriptors(): ByteArray { return blob.build() } +/** + * UAC2 DAC with an XMOS-style dual-clock tree, modeled on FiiO/Topping designs: + * clock selector 0x28 routes pin 1 → clock source 0x29 (44.1 kHz family) and + * pin 2 → clock source 0x2A (48 kHz family); the streaming terminal references the + * SELECTOR (not a source) via bCSourceID. One 32-bit async alt setting with feedback. + */ +internal fun uac2SelectorDacDescriptors(): ByteArray { + val blob = DescriptorBlob() + deviceAndConfigHeader(blob) + + // Interface 0 alt 0: AudioControl (UAC2) + blob.descriptor(0x04, 0x00, 0x00, 0x00, 0x01, 0x01, 0x20, 0x00) + // AC HEADER: bcdADC 2.00 + blob.descriptor(0x24, 0x01, 0x00, 0x02, 0x0A, 0x60, 0x00, 0x00) + // CLOCK_SOURCE 0x29 (44.1k family) + 0x2A (48k family), freq host-programmable + blob.descriptor(0x24, 0x0A, 0x29, 0x03, 0x07, 0x00, 0x00) + blob.descriptor(0x24, 0x0A, 0x2A, 0x03, 0x07, 0x00, 0x00) + // CLOCK_SELECTOR 0x28: 2 pins → 0x29, 0x2A; selector control host-programmable + blob.descriptor(0x24, 0x0B, 0x28, 0x02, 0x29, 0x2A, 0x03, 0x00) + // INPUT_TERMINAL 0x01: USB streaming, clock = SELECTOR 0x28, 2 channels + blob.descriptor( + 0x24, 0x02, 0x01, lo(0x0101), hi(0x0101), 0x00, 0x28, 0x02, + 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ) + // FEATURE_UNIT 0x0B: master mute+volume programmable + blob.descriptor( + 0x24, 0x06, 0x0B, 0x01, + 0x0F, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00 + ) + // OUTPUT_TERMINAL 0x03: headphones, source 0x0B, clock 0x28 + blob.descriptor(0x24, 0x03, 0x03, lo(0x0302), hi(0x0302), 0x00, 0x0B, 0x28, 0x00, 0x00, 0x00) + + // Interface 1 alt 0: AudioStreaming zero-bandwidth + blob.descriptor(0x04, 0x01, 0x00, 0x00, 0x01, 0x02, 0x20, 0x00) + // Interface 1 alt 1: 32-bit subslot + blob.descriptor(0x04, 0x01, 0x01, 0x02, 0x01, 0x02, 0x20, 0x00) + blob.descriptor( + 0x24, 0x01, 0x01, 0x05, 0x01, 0x01, 0x00, 0x00, 0x00, + 0x02, 0x03, 0x00, 0x00, 0x00, 0x00 + ) + blob.descriptor(0x24, 0x02, 0x01, 0x04, 0x20) + blob.descriptor(0x05, 0x01, 0x05, lo(1024), hi(1024), 0x01) + blob.descriptor(0x25, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00) + blob.descriptor(0x05, 0x81, 0x11, 0x04, 0x00, 0x04) + + return blob.build() +} + /** * UAC1 dongle modeled on an Apple-style USB-C adapter: adaptive iso OUT, two alt settings * (16-bit and 24-bit), discrete 44.1/48 kHz rates in the format descriptors, endpoint diff --git a/usbaudio/src/test/java/com/theveloper/pixelplay/usbaudio/descriptor/UacCapabilityProberTest.kt b/usbaudio/src/test/java/com/theveloper/pixelplay/usbaudio/descriptor/UacCapabilityProberTest.kt index edb97748a..7927179f5 100644 --- a/usbaudio/src/test/java/com/theveloper/pixelplay/usbaudio/descriptor/UacCapabilityProberTest.kt +++ b/usbaudio/src/test/java/com/theveloper/pixelplay/usbaudio/descriptor/UacCapabilityProberTest.kt @@ -71,6 +71,59 @@ class UacCapabilityProberTest { assertThat(caps.formats[0].sampleRatesHz).containsExactly(44_100, 88_200, 176_400).inOrder() } + @Test + fun `dual-clock selector device unions rates across both sources`() { + // 44.1k family behind pin 1 (clock 0x29), 48k family behind pin 2 (clock 0x2A). + val ratesByClock = mapOf( + 0x29 to rangeResponse( + Triple(44_100L, 44_100L, 0L), Triple(88_200L, 88_200L, 0L), + Triple(176_400L, 176_400L, 0L), Triple(352_800L, 352_800L, 0L) + ), + 0x2A to rangeResponse( + Triple(48_000L, 48_000L, 0L), Triple(96_000L, 96_000L, 0L), + Triple(192_000L, 192_000L, 0L), Triple(384_000L, 384_000L, 0L) + ) + ) + val transfer = UsbControlTransfer { _, request, value, index, buffer -> + assertThat(request).isEqualTo(0x02) + assertThat(value).isEqualTo(0x0100) + val response = ratesByClock[index shr 8] ?: return@UsbControlTransfer -1 + val n = minOf(buffer.size, response.size) + response.copyInto(buffer, endIndex = n) + n + } + + val caps = UacCapabilityProber.probe(topology(uac2SelectorDacDescriptors()), transfer) + + val format = caps.formats.single() + assertThat(format.sampleRatesHz).containsExactly( + 44_100, 48_000, 88_200, 96_000, 176_400, 192_000, 352_800, 384_000 + ).inOrder() + assertThat(format.clockSelectorId).isEqualTo(0x28) + assertThat(format.clockSources.map { it.clockSourceId }).containsExactly(0x29, 0x2A).inOrder() + assertThat(format.clockForRate(96_000)!!.clockSourceId).isEqualTo(0x2A) + assertThat(format.clockForRate(96_000)!!.selectorPin).isEqualTo(2) + assertThat(format.clockForRate(88_200)!!.clockSourceId).isEqualTo(0x29) + assertThat(format.clockForRate(88_200)!!.selectorPin).isEqualTo(1) + } + + @Test + fun `selector device with one dead clock still exposes the live one`() { + val liveResponse = rangeResponse(Triple(44_100L, 44_100L, 0L), Triple(88_200L, 88_200L, 0L)) + val transfer = UsbControlTransfer { _, _, _, index, buffer -> + if (index shr 8 != 0x29) return@UsbControlTransfer -1 + val n = minOf(buffer.size, liveResponse.size) + liveResponse.copyInto(buffer, endIndex = n) + n + } + + val caps = UacCapabilityProber.probe(topology(uac2SelectorDacDescriptors()), transfer) + + val format = caps.formats.single() + assertThat(format.sampleRatesHz).containsExactly(44_100, 88_200).inOrder() + assertThat(format.clockSources.single().clockSourceId).isEqualTo(0x29) + } + @Test fun `uac2 failed control transfer yields no formats rather than throwing`() { val failing = UsbControlTransfer { _, _, _, _, _ -> -1 } diff --git a/usbaudio/src/test/java/com/theveloper/pixelplay/usbaudio/descriptor/UsbDescriptorParserTest.kt b/usbaudio/src/test/java/com/theveloper/pixelplay/usbaudio/descriptor/UsbDescriptorParserTest.kt index e9f9a9f6f..c4c75677c 100644 --- a/usbaudio/src/test/java/com/theveloper/pixelplay/usbaudio/descriptor/UsbDescriptorParserTest.kt +++ b/usbaudio/src/test/java/com/theveloper/pixelplay/usbaudio/descriptor/UsbDescriptorParserTest.kt @@ -72,6 +72,38 @@ class UsbDescriptorParserTest { assertThat(unit.muteChannels).contains(0) } + // ─── UAC2 with a clock selector (XMOS dual-clock designs) ──────────────── + + @Test + fun `clock selector and both sources are parsed`() { + val topology = parseOk(uac2SelectorDacDescriptors()) + assertThat(topology.clockSelectors).hasSize(1) + val selector = topology.clockSelectors.single() + assertThat(selector.id).isEqualTo(0x28) + assertThat(selector.pinSourceIds).containsExactly(0x29, 0x2A).inOrder() + assertThat(topology.clockSources.map { it.id }).containsExactly(0x29, 0x2A) + } + + @Test + fun `clock path resolves through the selector with pin mapping`() { + val topology = parseOk(uac2SelectorDacDescriptors()) + val path = topology.clockPathFor(topology.playbackAltSettings.single()) + assertThat(path).isNotNull() + assertThat(path!!.selector?.id).isEqualTo(0x28) + assertThat(path.sources.map { it.id }).containsExactly(0x29, 0x2A).inOrder() + assertThat(path.pinBySourceId).containsExactly(0x29, 1, 0x2A, 2) + } + + @Test + fun `direct terminal-to-source link still resolves without a selector`() { + val topology = parseOk(uac2AsyncDacDescriptors()) + val path = topology.clockPathFor(topology.playbackAltSettings.first()) + assertThat(path).isNotNull() + assertThat(path!!.selector).isNull() + assertThat(path.sources.single().id).isEqualTo(0x29) + assertThat(path.pinBySourceId).isEmpty() + } + // ─── UAC1 ──────────────────────────────────────────────────────────────── @Test From 5dc7f870e209b41f10d835e0fc73ea61219f2130 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 4 Jul 2026 13:43:06 +0000 Subject: [PATCH 10/17] fix(usb): clickless skips/seeks/pauses via synthetic edge ramps; gapless resume; true volume slider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hardware testing on the BTR17 confirmed bit-perfect output up to 768 kHz but surfaced an audible crack when skipping songs. The cracks were step discontinuities in the iso driver: - flush() cut the waveform mid-sample (skip/seek click) - the stream consumed while a new track's first buffers trickled in, interleaving zero-gaps with audio (start-of-track crackle) - reconfigure cancelled in-flight transfers abruptly The driver's fill path is now a small state machine (PREFILL/APPROACH/PLAYING/DECAY/SILENCE): every silence-to-data and data-to-silence transition is bridged by a ~2 ms synthesized ramp, playback holds until ~40 ms is buffered after a start/flush (with a 50 ms deadline so short tails still play), flush discards exactly the bytes present at call time (race-free position marker, consumer-side skip), and reconfigure/teardown let the decay play out before cancelling. Real PCM samples remain bit-exact — the ramps exist only in gaps that were previously hard steps. Also: - UsbAudioSink now tracks play state and resumes a freshly configured stream itself, fixing a latent stall on gapless cross-format transitions where ExoPlayer never re-calls play() - the hardware volume slider initializes from the DAC's actual volume (new GET CUR support) instead of a hardcoded position Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FZMSPh3TgbRAMqgW4q1p6V --- .../data/service/player/usb/UsbAudioSink.kt | 8 + .../data/usb/UsbExclusiveModeController.kt | 10 + .../screens/UsbAudioSettingsScreen.kt | 8 +- .../viewmodel/UsbAudioSettingsViewModel.kt | 15 ++ .../service/player/usb/UsbAudioSinkTest.kt | 21 ++ .../usb/UsbExclusiveModeControllerTest.kt | 2 + usbaudio/src/main/cpp/iso_stream.cpp | 219 ++++++++++++++++-- usbaudio/src/main/cpp/iso_stream.h | 40 +++- usbaudio/src/main/cpp/jni_bridge.cpp | 31 ++- usbaudio/src/main/cpp/ring_buffer.h | 41 +++- usbaudio/src/main/cpp/uac_device.cpp | 12 + usbaudio/src/main/cpp/uac_device.h | 2 + .../pixelplay/usbaudio/UsbAudioNative.kt | 8 + .../pixelplay/usbaudio/UsbAudioSession.kt | 12 + 14 files changed, 399 insertions(+), 30 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/data/service/player/usb/UsbAudioSink.kt b/app/src/main/java/com/theveloper/pixelplay/data/service/player/usb/UsbAudioSink.kt index 6e083142f..1f44ed03e 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/service/player/usb/UsbAudioSink.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/service/player/usb/UsbAudioSink.kt @@ -65,6 +65,7 @@ class UsbAudioSink( private var playbackParameters = PlaybackParameters.DEFAULT private var skipSilenceEnabled = false private var audioAttributes: AudioAttributes? = null + private var playing = false // ─── Format support ────────────────────────────────────────────────────── @@ -127,6 +128,10 @@ class UsbAudioSink( inputFormat ) } + // A freshly configured stream starts paused (silent PLL priming). Mid-playback + // format changes — gapless 44.1→96 transitions — must resume it themselves: + // ExoPlayer only re-calls play() after user-initiated actions. + if (playing) session.resume() } this.inputFormat = inputFormat @@ -365,10 +370,12 @@ class UsbAudioSink( // ─── Transport ─────────────────────────────────────────────────────────── override fun play() { + playing = true session.resume() } override fun pause() { + playing = false session.pause() } @@ -403,6 +410,7 @@ class UsbAudioSink( override fun reset() { flush() + playing = false inputFormat = null sourceFormat = null sourceEncoding = null diff --git a/app/src/main/java/com/theveloper/pixelplay/data/usb/UsbExclusiveModeController.kt b/app/src/main/java/com/theveloper/pixelplay/data/usb/UsbExclusiveModeController.kt index 2c5953e03..e73bba37d 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/usb/UsbExclusiveModeController.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/usb/UsbExclusiveModeController.kt @@ -166,6 +166,16 @@ class UsbExclusiveModeController @Inject constructor( return session.setVolumeDb256(stepped) } + /** The DAC's current hardware volume as a 0..1 fraction of its range, or null. */ + fun hardwareVolumeFraction(): Float? { + val session = activeSession ?: return null + val range = session.volumeRangeDb256() ?: return null + val current = session.currentVolumeDb256() ?: return null + val (min, max) = range[0] to range[1] + if (max <= min) return null + return ((current - min).toFloat() / (max - min)).coerceIn(0f, 1f) + } + private suspend fun reconcile(enabled: Boolean, devices: List) { mutex.withLock { if (!enabled) { diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/UsbAudioSettingsScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/UsbAudioSettingsScreen.kt index ff5ca945d..79f86d26b 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/UsbAudioSettingsScreen.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/UsbAudioSettingsScreen.kt @@ -179,8 +179,10 @@ fun UsbAudioSettingsScreen( } item(key = "usb_volume_section") { + val hardwareVolume by viewModel.hardwareVolumeFraction.collectAsStateWithLifecycle() UsbVolumeSection( capabilities = capabilities, + initialVolumeFraction = hardwareVolume, maxVolumeAcknowledged = uiState.maxVolumeAcknowledged, onAcknowledge = viewModel::acknowledgeMaxVolume, onVolumeChange = viewModel::setHardwareVolume @@ -336,6 +338,7 @@ private fun UsbDeviceCard( @Composable private fun UsbVolumeSection( capabilities: UacCapabilities, + initialVolumeFraction: Float?, maxVolumeAcknowledged: Boolean, onAcknowledge: () -> Unit, onVolumeChange: (Float) -> Unit @@ -345,7 +348,10 @@ private fun UsbVolumeSection( icon = { Icon(Icons.Rounded.VolumeUp, null, tint = MaterialTheme.colorScheme.primary) } ) { if (capabilities.volume != null) { - var sliderValue by remember { mutableFloatStateOf(0.75f) } + // Snaps to the DAC's actual volume once the session reports it. + var sliderValue by remember(initialVolumeFraction) { + mutableFloatStateOf(initialVolumeFraction ?: 0.75f) + } Surface( color = MaterialTheme.colorScheme.surfaceContainer, shape = RoundedCornerShape(16.dp), diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/UsbAudioSettingsViewModel.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/UsbAudioSettingsViewModel.kt index b5c338f12..f9f5dacbe 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/UsbAudioSettingsViewModel.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/UsbAudioSettingsViewModel.kt @@ -22,9 +22,12 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.isActive import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import timber.log.Timber @HiltViewModel @@ -75,6 +78,18 @@ class UsbAudioSettingsViewModel @Inject constructor( viewModelScope.launch(Dispatchers.IO) { controller.setHardwareVolume(fraction) } } + /** The DAC's actual volume when a session comes up, so the slider starts truthful. */ + val hardwareVolumeFraction: StateFlow = controller.state + .map { state -> + if (state is UsbExclusiveState.Ready || state is UsbExclusiveState.Active) { + withContext(Dispatchers.IO) { controller.hardwareVolumeFraction() } + } else { + null + } + } + .distinctUntilChanged() + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null) + // ─── Debug: raw driver bring-up tone (bypasses ExoPlayer entirely) ──────── private var toneJob: Job? = null diff --git a/app/src/test/java/com/theveloper/pixelplay/data/service/player/usb/UsbAudioSinkTest.kt b/app/src/test/java/com/theveloper/pixelplay/data/service/player/usb/UsbAudioSinkTest.kt index f7be4fea0..eae265465 100644 --- a/app/src/test/java/com/theveloper/pixelplay/data/service/player/usb/UsbAudioSinkTest.kt +++ b/app/src/test/java/com/theveloper/pixelplay/data/service/player/usb/UsbAudioSinkTest.kt @@ -231,6 +231,27 @@ class UsbAudioSinkTest { assertThat(deadCalls).isEqualTo(1) } + @Test + fun `format change while playing resumes the new stream`() { + val sink = sink() + sink.configure(pcm16Format(rate = 44_100), 0, null) + sink.play() + io.mockk.verify(exactly = 1) { session.resume() } + + // Gapless transition into a different rate: nothing re-calls play(), so the + // sink itself must resume the freshly configured (paused) stream. + sink.configure(pcm16Format(rate = 96_000), 0, null) + io.mockk.verify(exactly = 2) { session.resume() } + } + + @Test + fun `format change while paused stays paused`() { + val sink = sink() + sink.configure(pcm16Format(rate = 44_100), 0, null) + sink.configure(pcm16Format(rate = 96_000), 0, null) + io.mockk.verify(exactly = 0) { session.resume() } + } + @Test fun `volume changes are ignored on the bit-perfect path`() { val sink = sink() diff --git a/app/src/test/java/com/theveloper/pixelplay/data/usb/UsbExclusiveModeControllerTest.kt b/app/src/test/java/com/theveloper/pixelplay/data/usb/UsbExclusiveModeControllerTest.kt index 48d9bfd49..fd6401848 100644 --- a/app/src/test/java/com/theveloper/pixelplay/data/usb/UsbExclusiveModeControllerTest.kt +++ b/app/src/test/java/com/theveloper/pixelplay/data/usb/UsbExclusiveModeControllerTest.kt @@ -224,6 +224,8 @@ class UsbExclusiveModeControllerTest { controller.sessionLost.test { enabledFlow.value = false + // Disabled is set only after the (IO-dispatched) close completes. + while (controller.state.value != UsbExclusiveState.Disabled) kotlinx.coroutines.yield() expectNoEvents() } assertThat(sessionCloseCount).isEqualTo(1) diff --git a/usbaudio/src/main/cpp/iso_stream.cpp b/usbaudio/src/main/cpp/iso_stream.cpp index 173cd6d9d..94f6ec795 100644 --- a/usbaudio/src/main/cpp/iso_stream.cpp +++ b/usbaudio/src/main/cpp/iso_stream.cpp @@ -1,7 +1,9 @@ #include "iso_stream.h" +#include #include #include +#include #include "log.h" #include "uac_device.h" @@ -10,6 +12,12 @@ namespace { constexpr int kTransfers = 8; constexpr int kPacketsPerTransferTarget = 8; constexpr unsigned kEventTimeoutUs = 100 * 1000; +// Synthetic silence↔data ramps: long enough to be clickless, short enough to be inaudible. +constexpr uint32_t kRampMs = 2; +// Audio buffered before playback starts after start/flush (absorbs bursty first buffers). +constexpr uint32_t kPrefillMs = 40; +// If some audio waits below the prefill threshold this long, start anyway (short tails). +constexpr uint32_t kPrefillDeadlineMs = 50; } // namespace IsoStream::IsoStream(UacDevice& device, Config config) @@ -51,6 +59,14 @@ bool IsoStream::start() { ring_ = std::make_unique(ringBytes > 4096 ? ringBytes : 4096); rateAccumulatorQ16_ = 0; + rampFrames_ = std::max(16, config_.rateHz * kRampMs / 1000); + prefillFrames_ = config_.rateHz * kPrefillMs / 1000; + prefillDeadlinePackets_ = + std::max(1, packetsPerSecond_ * kPrefillDeadlineMs / 1000); + prefillWaitPackets_ = 0; + fillState_ = FillState::kPrefill; + std::fill(std::begin(lastSample_), std::end(lastSample_), 0); + flushRequested_.store(false); stopping_.store(false); dead_.store(false); running_.store(true); @@ -132,8 +148,13 @@ void IsoStream::stop() { } void IsoStream::flush() { - if (ring_) ring_->clear(); - // In-flight audio will still play out (a few ms); consumedFrames() already includes it, + if (!ring_) return; + // The consumer thread discards exactly the bytes present now — anything the producer + // writes after this call is the new stream and survives. It also bridges the cut with + // a decay ramp instead of a hard step (the source of the audible click on skips). + discardUpToPos_.store(ring_->writePosition(), std::memory_order_release); + flushRequested_.store(true, std::memory_order_release); + // In-flight audio still plays out (a few ms); consumedFrames() already includes it, // which is exactly what the sink snapshots as its flush base. } @@ -162,33 +183,195 @@ uint32_t IsoStream::nextPacketFrames() { return frames; } +int32_t IsoStream::readWireSample(const uint8_t* src) const { + switch (config_.subslotBytes) { + case 2: + return static_cast(static_cast(src[0] | (src[1] << 8))) << 16; + case 3: + return static_cast(src[0] | (src[1] << 8) | (src[2] << 16)) << 8; + default: + return static_cast( + src[0] | (src[1] << 8) | (src[2] << 16) | + (static_cast(src[3]) << 24)); + } +} + +void IsoStream::writeWireSample(uint8_t* dst, int32_t s32top) const { + const uint32_t u = static_cast(s32top); + switch (config_.subslotBytes) { + case 2: + dst[0] = static_cast(u >> 16); + dst[1] = static_cast(u >> 24); + break; + case 3: + dst[0] = static_cast(u >> 8); + dst[1] = static_cast(u >> 16); + dst[2] = static_cast(u >> 24); + break; + default: + dst[0] = static_cast(u); + dst[1] = static_cast(u >> 8); + dst[2] = static_cast(u >> 16); + dst[3] = static_cast(u >> 24); + } +} + +void IsoStream::beginApproach() { + uint8_t firstFrame[kMaxChannels * 4]; + if (ring_->peek(firstFrame, frameBytes_) < static_cast(frameBytes_)) { + fillState_ = FillState::kSilence; + return; + } + for (int ch = 0; ch < config_.channels && ch < kMaxChannels; ++ch) { + rampTarget_[ch] = readWireSample(firstFrame + ch * config_.subslotBytes); + } + rampPos_ = 0; + prefillWaitPackets_ = 0; + fillState_ = FillState::kApproach; +} + +void IsoStream::beginDecay(FillState after) { + for (int ch = 0; ch < kMaxChannels; ++ch) rampTarget_[ch] = lastSample_[ch]; + rampPos_ = 0; + decayTarget_ = after; + fillState_ = FillState::kDecay; +} + +void IsoStream::handleFlushRequest() { + if (!flushRequested_.exchange(false, std::memory_order_acq_rel)) return; + // Discard exactly the audio that existed when flush() was called; bytes the producer + // wrote afterwards belong to the new stream and stay queued behind the prefill gate. + const uint64_t upTo = discardUpToPos_.load(std::memory_order_acquire); + const uint64_t readPos = ring_->readPosition(); + if (upTo > readPos) { + ring_->skip(static_cast(upTo - readPos)); + } + switch (fillState_) { + case FillState::kPlaying: + case FillState::kApproach: + beginDecay(FillState::kPrefill); + break; + case FillState::kDecay: + decayTarget_ = FillState::kPrefill; + break; + default: + fillState_ = FillState::kPrefill; + } + prefillWaitPackets_ = 0; +} + +uint32_t IsoStream::fillFrames(uint8_t* dst, uint32_t frames) { + const bool paused = paused_.load(std::memory_order_acquire); + const int channels = std::min(config_.channels, kMaxChannels); + uint32_t dataFrames = 0; + uint32_t remaining = frames; + uint8_t* cursor = dst; + + while (remaining > 0) { + switch (fillState_) { + case FillState::kPrefill: { + const uint32_t ringFrames = + static_cast(ring_->availableToRead() / frameBytes_); + if (!paused && ringFrames > 0) { + if (ringFrames >= prefillFrames_ || + prefillWaitPackets_ >= prefillDeadlinePackets_) { + beginApproach(); + continue; + } + ++prefillWaitPackets_; + } + std::memset(cursor, 0, static_cast(remaining) * frameBytes_); + return dataFrames; + } + + case FillState::kSilence: { + if (!paused && ring_->availableToRead() >= static_cast(frameBytes_)) { + beginApproach(); + continue; + } + std::memset(cursor, 0, static_cast(remaining) * frameBytes_); + return dataFrames; + } + + case FillState::kApproach: { + while (remaining > 0 && rampPos_ < rampFrames_) { + for (int ch = 0; ch < channels; ++ch) { + const int32_t value = static_cast( + static_cast(rampTarget_[ch]) * (rampPos_ + 1) / + (rampFrames_ + 1)); + writeWireSample(cursor + ch * config_.subslotBytes, value); + lastSample_[ch] = value; + } + cursor += frameBytes_; + --remaining; + ++rampPos_; + } + if (rampPos_ >= rampFrames_) fillState_ = FillState::kPlaying; + continue; + } + + case FillState::kPlaying: { + if (paused) { + beginDecay(FillState::kSilence); + continue; + } + const size_t got = + ring_->read(cursor, static_cast(remaining) * frameBytes_); + const uint32_t gotFrames = static_cast(got / frameBytes_); + if (gotFrames > 0) { + const uint8_t* lastFrame = cursor + (gotFrames - 1) * frameBytes_; + for (int ch = 0; ch < channels; ++ch) { + lastSample_[ch] = readWireSample(lastFrame + ch * config_.subslotBytes); + } + dataFrames += gotFrames; + remaining -= gotFrames; + cursor += static_cast(gotFrames) * frameBytes_; + } + if (remaining > 0) { + // Ran dry mid-stream (or the track just drained): bridge with a decay + // ramp instead of a hard step to zero. + xruns_.fetch_add(1, std::memory_order_relaxed); + beginDecay(FillState::kSilence); + continue; + } + return dataFrames; + } + + case FillState::kDecay: { + while (remaining > 0 && rampPos_ < rampFrames_) { + for (int ch = 0; ch < channels; ++ch) { + const int32_t value = static_cast( + static_cast(rampTarget_[ch]) * + (rampFrames_ - 1 - rampPos_) / rampFrames_); + writeWireSample(cursor + ch * config_.subslotBytes, value); + lastSample_[ch] = value; + } + cursor += frameBytes_; + --remaining; + ++rampPos_; + } + if (rampPos_ >= rampFrames_) fillState_ = decayTarget_; + continue; + } + } + } + return dataFrames; +} + void IsoStream::fillAndSubmit(TransferContext& context) { libusb_transfer* transfer = context.transfer; uint8_t* cursor = context.buffer.data(); uint32_t dataFrames = 0; int totalBytes = 0; - const bool silent = paused_.load(std::memory_order_acquire); + handleFlushRequest(); for (int p = 0; p < packetsPerTransfer_; ++p) { const uint32_t frames = nextPacketFrames(); const size_t bytes = static_cast(frames) * frameBytes_; - size_t got = 0; - if (!silent && bytes > 0) { - got = ring_->read(cursor, bytes); - if (got < bytes) { - std::memset(cursor + got, 0, bytes - got); - // Count as an underrun only when audio actually ran dry mid-stream — - // steady silence at a track boundary / drain is not an xrun. - if (got > 0 || lastPacketHadData_) { - xruns_.fetch_add(1, std::memory_order_relaxed); - } - } - lastPacketHadData_ = got > 0; - } else if (bytes > 0) { - std::memset(cursor, 0, bytes); + if (bytes > 0) { + dataFrames += fillFrames(cursor, frames); } - dataFrames += static_cast(got / frameBytes_); transfer->iso_packet_desc[p].length = static_cast(bytes); cursor += bytes; totalBytes += static_cast(bytes); diff --git a/usbaudio/src/main/cpp/iso_stream.h b/usbaudio/src/main/cpp/iso_stream.h index 720734d8d..50ae8644a 100644 --- a/usbaudio/src/main/cpp/iso_stream.h +++ b/usbaudio/src/main/cpp/iso_stream.h @@ -52,7 +52,10 @@ class IsoStream { void resume() { paused_.store(false, std::memory_order_release); } bool paused() const { return paused_.load(std::memory_order_acquire); } - /** Drops all buffered (not yet consumed) audio. */ + /** + * Drops all audio buffered up to this moment (data written afterwards is kept). + * The consumer bridges the cut with a short synthetic decay ramp — no click. + */ void flush(); /** Producer side; returns bytes accepted (0 = ring full), -1 when the stream is dead. */ @@ -77,10 +80,30 @@ class IsoStream { bool inFlight = false; }; + /** + * Consumer-side output shaping. Real PCM is passed through bit-exact; the only + * synthesized audio is a ~2 ms ramp bridging silence↔data transitions, killing the + * step-discontinuity click on skip/seek/pause/underrun. + */ + enum class FillState { + kPrefill, // after start/flush: hold silence until the ring has enough audio + kApproach, // synthetic ramp 0 → first pending sample + kPlaying, // bit-exact passthrough from the ring + kDecay, // synthetic ramp last sample → 0 + kSilence // steady silence (paused or ran dry); resumes via kApproach + }; + static void onTransferComplete(libusb_transfer* transfer); static void onFeedbackComplete(libusb_transfer* transfer); void fillAndSubmit(TransferContext& context); + /** Fills `frames` frames at dst per the state machine; returns real data frames. */ + uint32_t fillFrames(uint8_t* dst, uint32_t frames); + void handleFlushRequest(); + void beginApproach(); + void beginDecay(FillState after); + int32_t readWireSample(const uint8_t* src) const; + void writeWireSample(uint8_t* dst, int32_t s32top) const; bool submitFeedback(); void markDead(const char* why); void eventLoop(); @@ -100,7 +123,20 @@ class IsoStream { uint64_t nominalRateQ16_ = 0; std::atomic feedbackRateQ16_{0}; // 0 = no feedback yet uint64_t rateAccumulatorQ16_ = 0; // event-thread only - bool lastPacketHadData_ = false; // event-thread only + + // ─── Fill state machine (event-thread only unless noted) ─────────────── + static constexpr int kMaxChannels = 8; + FillState fillState_ = FillState::kPrefill; + FillState decayTarget_ = FillState::kSilence; + int32_t lastSample_[kMaxChannels] = {0}; + int32_t rampTarget_[kMaxChannels] = {0}; + uint32_t rampPos_ = 0; + uint32_t rampFrames_ = 0; // ~2 ms at the stream rate + uint32_t prefillFrames_ = 0; // ~40 ms of audio before leaving kPrefill + uint32_t prefillWaitPackets_ = 0; // deadline fallback so short tails still play + uint32_t prefillDeadlinePackets_ = 0; + std::atomic flushRequested_{false}; + std::atomic discardUpToPos_{0}; // ring write position captured at flush() std::unique_ptr ring_; std::vector> transfers_; diff --git a/usbaudio/src/main/cpp/jni_bridge.cpp b/usbaudio/src/main/cpp/jni_bridge.cpp index 3e29f07b8..3b417df9a 100644 --- a/usbaudio/src/main/cpp/jni_bridge.cpp +++ b/usbaudio/src/main/cpp/jni_bridge.cpp @@ -1,8 +1,10 @@ #include #include +#include #include #include +#include #include #include "iso_stream.h" @@ -30,6 +32,18 @@ std::shared_ptr streamOf(jlong handle) { return session->stream; } +/** + * Lets the decay ramp play out (pause → ~2 ms ramp to silence within the next transfer + * cycle) before cancelling, so reconfigures/teardowns don't cut audio mid-sample. + */ +void gracefulStop(IsoStream& stream) { + if (stream.alive() && !stream.paused()) { + stream.pause(); + std::this_thread::sleep_for(std::chrono::milliseconds(25)); + } + stream.stop(); +} + } // namespace extern "C" { @@ -87,7 +101,7 @@ Java_com_theveloper_pixelplay_usbaudio_UsbAudioNative_nativeConfigureStream( std::lock_guard lock(session->mutex); if (session->stream) { - session->stream->stop(); + gracefulStop(*session->stream); session->stream.reset(); } if (!session->device->setAltSetting(asInterface, altSetting)) { @@ -192,7 +206,7 @@ Java_com_theveloper_pixelplay_usbaudio_UsbAudioNative_nativeStop( if (session == nullptr) return -1; std::lock_guard lock(session->mutex); if (session->stream) { - session->stream->stop(); + gracefulStop(*session->stream); session->stream.reset(); } // Alt setting 0 releases the reserved isochronous bandwidth. @@ -263,6 +277,19 @@ Java_com_theveloper_pixelplay_usbaudio_UsbAudioNative_nativeGetVolumeRangeDb256( return out; } +JNIEXPORT jint JNICALL +Java_com_theveloper_pixelplay_usbaudio_UsbAudioNative_nativeGetVolumeDb256( + JNIEnv*, jobject, jlong handle, jint uacVersion, jint unitId, jint acInterface) { + NativeSession* session = fromHandle(handle); + if (session == nullptr) return INT32_MIN; + std::lock_guard lock(session->mutex); + int32_t value = 0; + if (!session->device->getVolumeDb256(uacVersion, unitId, acInterface, &value)) { + return INT32_MIN; + } + return value; +} + JNIEXPORT jint JNICALL Java_com_theveloper_pixelplay_usbaudio_UsbAudioNative_nativeSetVolumeDb256( JNIEnv*, jobject, jlong handle, jint uacVersion, jint unitId, jint acInterface, jint valueDb256) { diff --git a/usbaudio/src/main/cpp/ring_buffer.h b/usbaudio/src/main/cpp/ring_buffer.h index 540968bb9..6b729fd6c 100644 --- a/usbaudio/src/main/cpp/ring_buffer.h +++ b/usbaudio/src/main/cpp/ring_buffer.h @@ -46,6 +46,40 @@ class RingBuffer { /** Copies up to `size` bytes out; returns the number read. */ size_t read(uint8_t* out, size_t size) { + const size_t toRead = copyOut(out, size); + if (toRead > 0) { + readPos_.store(readPos_.load(std::memory_order_relaxed) + toRead, + std::memory_order_release); + } + return toRead; + } + + /** Like read() but leaves the data in the ring (consumer side only). */ + size_t peek(uint8_t* out, size_t size) const { return copyOut(out, size); } + + /** Consumer-side: discards up to `size` bytes; returns the number skipped. */ + size_t skip(size_t size) { + const uint64_t readPos = readPos_.load(std::memory_order_relaxed); + const uint64_t writePos = writePos_.load(std::memory_order_acquire); + const size_t available = static_cast(writePos - readPos); + const size_t toSkip = size < available ? size : available; + if (toSkip > 0) { + readPos_.store(readPos + toSkip, std::memory_order_release); + } + return toSkip; + } + + uint64_t readPosition() const { return readPos_.load(std::memory_order_acquire); } + uint64_t writePosition() const { return writePos_.load(std::memory_order_acquire); } + + /** Consumer-side discard of everything currently buffered. */ + void clear() { + readPos_.store(writePos_.load(std::memory_order_acquire), std::memory_order_release); + } + +private: + /** Shared copy path of read()/peek(); does not advance the read position. */ + size_t copyOut(uint8_t* out, size_t size) const { const uint64_t readPos = readPos_.load(std::memory_order_relaxed); const uint64_t writePos = writePos_.load(std::memory_order_acquire); const size_t available = static_cast(writePos - readPos); @@ -58,16 +92,9 @@ class RingBuffer { if (toRead > first) { std::memcpy(out + first, buffer_.data(), toRead - first); } - readPos_.store(readPos + toRead, std::memory_order_release); return toRead; } - /** Consumer-side discard of everything currently buffered. */ - void clear() { - readPos_.store(writePos_.load(std::memory_order_acquire), std::memory_order_release); - } - -private: std::vector buffer_; std::atomic writePos_{0}; std::atomic readPos_{0}; diff --git a/usbaudio/src/main/cpp/uac_device.cpp b/usbaudio/src/main/cpp/uac_device.cpp index 3e4da0c72..a1e3b6195 100644 --- a/usbaudio/src/main/cpp/uac_device.cpp +++ b/usbaudio/src/main/cpp/uac_device.cpp @@ -181,6 +181,18 @@ bool UacDevice::getVolumeRangeDb256(int uacVersion, int unitId, int acInterface, return true; } +bool UacDevice::getVolumeDb256(int uacVersion, int unitId, int acInterface, int32_t* out) { + const uint16_t value = static_cast(kFuVolumeControl << 8); // channel 0 = master + const uint16_t index = static_cast((unitId << 8) | (acInterface & 0xFF)); + // UAC2 uses CUR with the class GET direction; UAC1 has a dedicated GET_CUR request. + const uint8_t request = uacVersion == 2 ? kReqCur : 0x81; + uint8_t buffer[2] = {0}; + const int read = controlIn(kClassInterfaceIn, request, value, index, buffer, sizeof(buffer)); + if (read < 2) return false; + *out = static_cast(buffer[0] | (buffer[1] << 8)); + return true; +} + bool UacDevice::setVolumeDb256(int uacVersion, int unitId, int acInterface, int32_t valueDb256) { (void)uacVersion; // SET CUR encoding is identical for UAC1 and UAC2 (2-byte value) const int16_t clamped = static_cast(valueDb256); diff --git a/usbaudio/src/main/cpp/uac_device.h b/usbaudio/src/main/cpp/uac_device.h index 37de8100d..2cdc1d331 100644 --- a/usbaudio/src/main/cpp/uac_device.h +++ b/usbaudio/src/main/cpp/uac_device.h @@ -36,6 +36,8 @@ class UacDevice { /** Volume values are UAC-native: signed 1/256 dB steps. out = {min, max, res}. */ bool getVolumeRangeDb256(int uacVersion, int unitId, int acInterface, int32_t out[3]); + /** Reads the feature unit's current master volume (1/256 dB). */ + bool getVolumeDb256(int uacVersion, int unitId, int acInterface, int32_t* out); bool setVolumeDb256(int uacVersion, int unitId, int acInterface, int32_t valueDb256); bool setMute(int uacVersion, int unitId, int acInterface, bool mute); diff --git a/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/UsbAudioNative.kt b/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/UsbAudioNative.kt index 25efbf3b1..ddad1f6d9 100644 --- a/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/UsbAudioNative.kt +++ b/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/UsbAudioNative.kt @@ -132,6 +132,14 @@ object UsbAudioNative { valueDb256: Int ): Int + /** Current master volume in 1/256 dB, or [Int.MIN_VALUE] when unreadable. */ + external fun nativeGetVolumeDb256( + handle: Long, + uacVersion: Int, + unitId: Int, + acInterface: Int + ): Int + external fun nativeSetMute( handle: Long, uacVersion: Int, diff --git a/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/UsbAudioSession.kt b/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/UsbAudioSession.kt index 9e66dcc5c..8eba39311 100644 --- a/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/UsbAudioSession.kt +++ b/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/UsbAudioSession.kt @@ -201,6 +201,18 @@ class UsbAudioSession private constructor( } } + /** The DAC's current master volume in 1/256 dB, or null when unreadable. */ + fun currentVolumeDb256(): Int? { + synchronized(lock) { + if (closed) return null + val unit = capabilities.volume ?: return null + val value = UsbAudioNative.nativeGetVolumeDb256( + handle, uacVersionCode, unit.featureUnitId, capabilities.controlInterfaceNumber + ) + return if (value == Int.MIN_VALUE) null else value + } + } + fun setVolumeDb256(valueDb256: Int): Boolean { synchronized(lock) { if (closed) return false From f631b469320ac82ee409d83d8f2386dbeb1547f0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 4 Jul 2026 18:33:32 +0000 Subject: [PATCH 11/17] ci: dispatchable workflow publishing signed beta APKs as a GitHub Release Builds the chosen ref with the nightly-style keystore (shared cache key, so betas and nightlies stay update-compatible) and attaches the split release APKs to a tagged GitHub Release. Verified locally first: the first R8-minified build containing :usbaudio succeeds, both .so files are packaged, and the JNI surface survives shrinking unrenamed. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FZMSPh3TgbRAMqgW4q1p6V --- .github/workflows/usb-beta-release.yml | 115 +++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 .github/workflows/usb-beta-release.yml diff --git a/.github/workflows/usb-beta-release.yml b/.github/workflows/usb-beta-release.yml new file mode 100644 index 000000000..4e5f933dc --- /dev/null +++ b/.github/workflows/usb-beta-release.yml @@ -0,0 +1,115 @@ +name: USB Beta Release + +# Manually dispatched: builds signed release APKs from the chosen ref and publishes +# them as a GitHub Release, so testers can download without an Actions login. +on: + workflow_dispatch: + inputs: + tag: + description: 'Release tag (also used in the APK file names)' + required: true + default: 'v0.7.5-usb-beta.1' + prerelease: + description: 'Mark as pre-release' + required: true + default: true + type: boolean + +permissions: + contents: write + +concurrency: + group: usb-beta-release + cancel-in-progress: false + +jobs: + release: + if: github.actor != 'dependabot[bot]' + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Set up JDK 21 + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: '21' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v6 + + # Same cache key as nightly-apk.yml: beta releases and nightlies share one + # signature, so update-installs work across them. + - name: Cache nightly keystore + id: cache-keystore + uses: actions/cache@v5 + with: + path: vz-pixelplay.jks + key: ${{ runner.os }}-nightly-keystore-vz-pixelplay + + - name: Generate nightly keystore if not cached + if: steps.cache-keystore.outputs.cache-hit != 'true' + run: | + keytool -genkey -v -keystore vz-pixelplay.jks -alias pixelplay-nightly-key -keyalg RSA -keysize 4096 -validity 10000 \ + -storepass 994273 -keypass 994273 \ + -dname "CN=PixelPlay Nightly, OU=Dev, O=PixelPlay, L=World, S=World, C=US" + + - name: Create keystore.properties + run: | + echo "storePassword=994273" > keystore.properties + echo "keyAlias=pixelplay-nightly-key" >> keystore.properties + echo "keyPassword=994273" >> keystore.properties + + - name: Build release APKs + run: gradle :app:assembleRelease -Ppixelplay.enableAbiSplits=true + + - name: Verify split APKs + run: | + BUILD_TOOLS_VERSION="$(ls "$ANDROID_HOME/build-tools" | sort -V | tail -n 1)" + for apk in \ + app/build/outputs/apk/release/app-arm64-v8a-release.apk \ + app/build/outputs/apk/release/app-armeabi-v7a-release.apk + do + "$ANDROID_HOME/build-tools/$BUILD_TOOLS_VERSION/aapt2" dump badging "$apk" >/dev/null + "$ANDROID_HOME/build-tools/$BUILD_TOOLS_VERSION/apksigner" verify --verbose "$apk" + done + + - name: Stage release assets + run: | + mkdir -p release-assets + cp app/build/outputs/apk/release/app-arm64-v8a-release.apk \ + "release-assets/PixelPlay-${{ inputs.tag }}-arm64-v8a.apk" + cp app/build/outputs/apk/release/app-armeabi-v7a-release.apk \ + "release-assets/PixelPlay-${{ inputs.tag }}-armeabi-v7a.apk" + + - name: Publish GitHub release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ inputs.tag }} + target_commitish: ${{ github.sha }} + name: 'USB Exclusive Mode (bit-perfect) — ${{ inputs.tag }}' + prerelease: ${{ inputs.prerelease }} + files: release-assets/*.apk + body: | + Beta build of **USB exclusive mode**: PixelPlay claims a USB DAC directly + (libusb, no root) and streams at the track's native sample rate and bit + depth — bit-perfect, bypassing the Android mixer. Validated on a FiiO + BTR17 up to 768 kHz. + + **Install:** download the `arm64-v8a` APK (fits virtually all phones; + `armeabi-v7a` is only for very old 32-bit devices), open it on the phone + and allow installing from unknown sources. + + **Signature note:** this build is signed with the CI test key (same as the + nightly builds). It cannot update-install over a copy of PixelPlay signed + with a different key — uninstall that one first, or install alongside a + `.debug` test build (different application id, no conflict). + + **Usage:** Settings → Playback → USB Audio → enable *USB exclusive mode*, + plug in the DAC, grant the USB permission. The Now Playing screen shows + `USB • /` while bit-perfect output is active, and the DAC's own + display follows the track's sample rate. + + Full feature documentation: `docs/usb-exclusive-mode.md` in the repo. From 1e5b0ec9089c5397bc83cd6e11b94e1d561f72a4 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 4 Jul 2026 19:15:19 +0000 Subject: [PATCH 12/17] ci: allow triggering the beta release workflow by tag push Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FZMSPh3TgbRAMqgW4q1p6V --- .github/workflows/usb-beta-release.yml | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/.github/workflows/usb-beta-release.yml b/.github/workflows/usb-beta-release.yml index 4e5f933dc..6155fcc57 100644 --- a/.github/workflows/usb-beta-release.yml +++ b/.github/workflows/usb-beta-release.yml @@ -1,8 +1,12 @@ name: USB Beta Release -# Manually dispatched: builds signed release APKs from the chosen ref and publishes -# them as a GitHub Release, so testers can download without an Actions login. +# Builds signed release APKs and publishes them as a GitHub Release, so testers can +# download without an Actions login. Trigger by pushing a `v*-usb-beta*` tag, or +# dispatch manually against any ref. on: + push: + tags: + - 'v*-usb-beta*' workflow_dispatch: inputs: tag: @@ -77,20 +81,21 @@ jobs: done - name: Stage release assets + env: + RELEASE_TAG: ${{ inputs.tag || github.ref_name }} run: | mkdir -p release-assets cp app/build/outputs/apk/release/app-arm64-v8a-release.apk \ - "release-assets/PixelPlay-${{ inputs.tag }}-arm64-v8a.apk" + "release-assets/PixelPlay-${RELEASE_TAG}-arm64-v8a.apk" cp app/build/outputs/apk/release/app-armeabi-v7a-release.apk \ - "release-assets/PixelPlay-${{ inputs.tag }}-armeabi-v7a.apk" + "release-assets/PixelPlay-${RELEASE_TAG}-armeabi-v7a.apk" - name: Publish GitHub release uses: softprops/action-gh-release@v2 with: - tag_name: ${{ inputs.tag }} - target_commitish: ${{ github.sha }} - name: 'USB Exclusive Mode (bit-perfect) — ${{ inputs.tag }}' - prerelease: ${{ inputs.prerelease }} + tag_name: ${{ inputs.tag || github.ref_name }} + name: 'USB Exclusive Mode (bit-perfect) — ${{ inputs.tag || github.ref_name }}' + prerelease: ${{ inputs.prerelease || true }} files: release-assets/*.apk body: | Beta build of **USB exclusive mode**: PixelPlay claims a USB DAC directly From e5913a3b8b86bdbe9bb9f0054581e5ff58282fbf Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 4 Jul 2026 19:34:46 +0000 Subject: [PATCH 13/17] ci: cut usb beta release v0.7.5-usb-beta.1 via branch-push trigger The workflow-dispatch API and tag pushes are unavailable from this environment, so the release workflow also fires on pushes touching .github/release-tag.txt and reads the tag from that file. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FZMSPh3TgbRAMqgW4q1p6V --- .github/release-tag.txt | 1 + .github/workflows/usb-beta-release.yml | 46 +++++++++++++++++--------- 2 files changed, 32 insertions(+), 15 deletions(-) create mode 100644 .github/release-tag.txt diff --git a/.github/release-tag.txt b/.github/release-tag.txt new file mode 100644 index 000000000..32eef0351 --- /dev/null +++ b/.github/release-tag.txt @@ -0,0 +1 @@ +v0.7.5-usb-beta.1 diff --git a/.github/workflows/usb-beta-release.yml b/.github/workflows/usb-beta-release.yml index 6155fcc57..df27bd930 100644 --- a/.github/workflows/usb-beta-release.yml +++ b/.github/workflows/usb-beta-release.yml @@ -1,23 +1,23 @@ name: USB Beta Release -# Builds signed release APKs and publishes them as a GitHub Release, so testers can -# download without an Actions login. Trigger by pushing a `v*-usb-beta*` tag, or -# dispatch manually against any ref. +# Builds signed release APKs and publishes them as a GitHub Release. +# Two triggers: +# - manual dispatch (tag comes from the input), or +# - a push that changes .github/release-tag.txt (tag comes from that file) — this +# lets a release be cut from an environment that can push the branch but cannot +# call the workflow-dispatch API or push tags. on: - push: - tags: - - 'v*-usb-beta*' workflow_dispatch: inputs: tag: description: 'Release tag (also used in the APK file names)' required: true default: 'v0.7.5-usb-beta.1' - prerelease: - description: 'Mark as pre-release' - required: true - default: true - type: boolean + push: + branches: + - claude/new-session-lpe3cm + paths: + - '.github/release-tag.txt' permissions: contents: write @@ -35,6 +35,22 @@ jobs: - name: Checkout repository uses: actions/checkout@v6 + - name: Resolve release tag + id: tag + env: + INPUT_TAG: ${{ inputs.tag }} + run: | + if [ -n "${INPUT_TAG}" ]; then + TAG="${INPUT_TAG}" + else + TAG="$(tr -d '[:space:]' < .github/release-tag.txt)" + fi + if [ -z "${TAG}" ]; then + echo "No release tag resolved" >&2 + exit 1 + fi + echo "tag=${TAG}" >> "$GITHUB_OUTPUT" + - name: Set up JDK 21 uses: actions/setup-java@v5 with: @@ -82,7 +98,7 @@ jobs: - name: Stage release assets env: - RELEASE_TAG: ${{ inputs.tag || github.ref_name }} + RELEASE_TAG: ${{ steps.tag.outputs.tag }} run: | mkdir -p release-assets cp app/build/outputs/apk/release/app-arm64-v8a-release.apk \ @@ -93,9 +109,9 @@ jobs: - name: Publish GitHub release uses: softprops/action-gh-release@v2 with: - tag_name: ${{ inputs.tag || github.ref_name }} - name: 'USB Exclusive Mode (bit-perfect) — ${{ inputs.tag || github.ref_name }}' - prerelease: ${{ inputs.prerelease || true }} + tag_name: ${{ steps.tag.outputs.tag }} + name: 'USB Exclusive Mode (bit-perfect) — ${{ steps.tag.outputs.tag }}' + prerelease: true files: release-assets/*.apk body: | Beta build of **USB exclusive mode**: PixelPlay claims a USB DAC directly From d4b1e489fba537c8752f3c7ec9ffe8470de38d99 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 8 Jul 2026 21:44:14 +0000 Subject: [PATCH 14/17] feat(usb): software volume safety layer for DACs without hardware volume MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DACs with no UAC feature unit previously played at fixed full scale — dangerous straight into sensitive IEMs. They now get a software gain stage: - PcmRepacker: single Q16 fixed-point multiply in the 32-bit domain before subslot packing; unity gain (0 dB) skips the multiply and stays the untouched bit-perfect path. FormatNegotiator gains preferDeepestDepth so attenuated 16-bit material lands in a 24/32-bit subslot and keeps its low-order bits. - Safe by default: first connect engages −30 dB; levels above −10 dB stay locked behind an explicit loudness acknowledgement; the chosen level is remembered per device (DataStore). - Volume keys work in exclusive mode: the media-session player is wrapped in a ForwardingPlayer advertising remote device volume (30 steps) that maps onto the hardware feature unit when present, else the software gain range, with mute support on both paths. - Honest labeling: the Now Playing badge appends the attenuation (USB • 16bit/44.1kHz • −30dB) whenever samples are scaled — "bit-perfect" is only ever claimed at unity. - Purist escape hatch: a "Fixed line-level output" toggle bypasses the gain stage entirely and restores full-scale output with the old warning card. Settings screen gets the software dB slider (−60..0), the unlock card, and the fixed-output toggle (en/es/fr strings). Covered by new unit tests: repack gain goldens, deepest-depth negotiation, controller ack-gating/persistence/volume-key mapping. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FZMSPh3TgbRAMqgW4q1p6V --- .../preferences/UserPreferencesRepository.kt | 9 + .../pixelplay/data/service/MusicService.kt | 26 ++- .../data/service/player/usb/PcmRepacker.kt | 25 ++- .../data/service/player/usb/UsbAudioSink.kt | 13 +- .../player/usb/UsbDeviceVolumePlayer.kt | 65 ++++++ .../pixelplay/data/usb/UsbDeviceInfo.kt | 18 +- .../data/usb/UsbExclusiveModeController.kt | 195 +++++++++++++++++- .../screens/UsbAudioSettingsScreen.kt | 140 ++++++++++--- .../presentation/viewmodel/PlayerViewModel.kt | 9 +- .../viewmodel/UsbAudioSettingsViewModel.kt | 14 ++ .../main/res/values-es/strings_settings.xml | 8 + .../main/res/values-fr/strings_settings.xml | 8 + app/src/main/res/values/strings_settings.xml | 8 + .../service/player/usb/PcmRepackerTest.kt | 78 +++++++ .../service/player/usb/UsbAudioSinkTest.kt | 6 +- .../usb/UsbExclusiveModeControllerTest.kt | 95 +++++++++ docs/usb-exclusive-mode.md | 32 ++- .../pixelplay/usbaudio/UsbAudioSession.kt | 9 + .../usbaudio/negotiation/FormatNegotiator.kt | 25 ++- .../negotiation/FormatNegotiatorTest.kt | 27 +++ 20 files changed, 751 insertions(+), 59 deletions(-) create mode 100644 app/src/main/java/com/theveloper/pixelplay/data/service/player/usb/UsbDeviceVolumePlayer.kt diff --git a/app/src/main/java/com/theveloper/pixelplay/data/preferences/UserPreferencesRepository.kt b/app/src/main/java/com/theveloper/pixelplay/data/preferences/UserPreferencesRepository.kt index 8f835e44e..e1a0b6860 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/preferences/UserPreferencesRepository.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/preferences/UserPreferencesRepository.kt @@ -144,6 +144,7 @@ class UserPreferencesRepository @Inject constructor( val USB_EXCLUSIVE_MODE_ENABLED = booleanPreferencesKey("usb_exclusive_mode_enabled") val USB_REMEMBERED_DEVICES = stringPreferencesKey("usb_remembered_devices_json") val USB_EXCLUSIVE_MAX_VOLUME_ACK = booleanPreferencesKey("usb_exclusive_max_volume_ack") + val USB_FIXED_VOLUME_OUTPUT = booleanPreferencesKey("usb_fixed_volume_output") val CROSSFADE_DURATION = intPreferencesKey("crossfade_duration") val CUSTOM_GENRES = stringSetPreferencesKey("custom_genres") val CUSTOM_GENRE_ICONS = stringPreferencesKey("custom_genre_icons") @@ -379,6 +380,14 @@ class UserPreferencesRepository @Inject constructor( dataStore.edit { it[PreferencesKeys.USB_EXCLUSIVE_MAX_VOLUME_ACK] = acknowledged } } + /** Purist mode: always full-scale output on volume-less DACs (no software gain stage). */ + val usbFixedVolumeOutputFlow: Flow = + pref { it[PreferencesKeys.USB_FIXED_VOLUME_OUTPUT] ?: false } + + suspend fun setUsbFixedVolumeOutput(fixed: Boolean) { + dataStore.edit { it[PreferencesKeys.USB_FIXED_VOLUME_OUTPUT] = fixed } + } + val keepPlayingInBackgroundFlow: Flow = pref { it[PreferencesKeys.KEEP_PLAYING_IN_BACKGROUND] ?: true } diff --git a/app/src/main/java/com/theveloper/pixelplay/data/service/MusicService.kt b/app/src/main/java/com/theveloper/pixelplay/data/service/MusicService.kt index 2ab97f5fb..f8f85dcaf 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/service/MusicService.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/service/MusicService.kt @@ -347,13 +347,33 @@ class MusicService : MediaLibraryService() { replayGainProcessor.onTransitionFinished() } + /** + * Wraps the player for the media session while USB exclusive mode is engaged so the + * phone's volume keys reach the DAC (hardware feature unit or software gain stage). + * Reuses the existing wrapper when the underlying player hasn't changed, so the + * identity check below keeps working. + */ + private var usbVolumePlayerWrapper: com.theveloper.pixelplay.data.service.player.usb.UsbDeviceVolumePlayer? = null + + private fun sessionPlayerFor(player: Player): Player { + if (usbExclusiveModeController.activeSession == null) { + usbVolumePlayerWrapper = null + return player + } + usbVolumePlayerWrapper?.takeIf { it.wrappedPlayer === player }?.let { return it } + return com.theveloper.pixelplay.data.service.player.usb.UsbDeviceVolumePlayer( + player, usbExclusiveModeController + ).also { usbVolumePlayerWrapper = it } + } + private fun publishMediaSessionPlayer(player: Player, logMessage: String) { val session = mediaSession ?: return + val target = sessionPlayerFor(player) val oldPlayer = session.player - if (oldPlayer !== player) { + if (oldPlayer !== target) { oldPlayer.removeListener(playerListener) - session.player = player - player.addListener(playerListener) + session.player = target + target.addListener(playerListener) } Timber.tag("MusicService").d(logMessage) diff --git a/app/src/main/java/com/theveloper/pixelplay/data/service/player/usb/PcmRepacker.kt b/app/src/main/java/com/theveloper/pixelplay/data/service/player/usb/PcmRepacker.kt index 25852ca17..3d6dca227 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/service/player/usb/PcmRepacker.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/service/player/usb/PcmRepacker.kt @@ -26,6 +26,16 @@ object PcmRepacker { FLOAT(4) } + /** Unity gain in Q16 fixed point — the bit-perfect passthrough value. */ + const val UNITY_GAIN_Q16: Int = 1 shl 16 + + /** Converts an attenuation in dB (≤ 0) to Q16 gain; null/0 dB → [UNITY_GAIN_Q16]. */ + fun gainQ16FromDb(db: Float?): Int = when { + db == null || db >= 0f -> UNITY_GAIN_Q16 + db <= -90f -> 0 + else -> (Math.pow(10.0, db / 20.0) * UNITY_GAIN_Q16).toInt() + } + /** Bytes [repack] will produce for [inputBytes] of source data. */ fun outputSize(inputBytes: Int, source: Encoding, sourceChannels: Int, targetChannels: Int, subslotBytes: Int): Int { val frameBytes = source.bytesPerSample * sourceChannels @@ -40,6 +50,12 @@ object PcmRepacker { * order. Channel handling: equal counts copy through; mono duplicates into the first two * target channels; missing channels are silence; excess source channels are dropped * (callers downmix 5.1/7.1 upstream — dropping is only the last-resort fallback). + * + * [gainQ16] is the software-volume stage for DACs without a hardware volume control: + * at [UNITY_GAIN_Q16] samples pass through untouched (the bit-perfect path takes no + * multiplication); below unity, scaling happens here in the 32-bit domain — before + * subslot packing — so attenuated 16-bit material keeps full fidelity into a + * 24/32-bit subslot. */ fun repack( input: ByteBuffer, @@ -47,13 +63,15 @@ object PcmRepacker { sourceChannels: Int, targetChannels: Int, subslotBytes: Int, - output: ByteBuffer + output: ByteBuffer, + gainQ16: Int = UNITY_GAIN_Q16 ) { val in_ = input.duplicate().order(ByteOrder.LITTLE_ENDIAN) output.order(ByteOrder.LITTLE_ENDIAN) val frameBytes = source.bytesPerSample * sourceChannels val frames = (in_.limit() - in_.position()) / frameBytes + val applyGain = gainQ16 != UNITY_GAIN_Q16 val copyChannels = minOf(sourceChannels, targetChannels) for (frame in 0 until frames) { val frameBase = in_.position() + frame * frameBytes @@ -63,11 +81,14 @@ object PcmRepacker { sourceChannels == 1 && channel == 1 -> 0 // mono → duplicate into R else -> -1 // silence } - val s32top = if (sourceChannel >= 0) { + var s32top = if (sourceChannel >= 0) { readS32Top(in_, frameBase + sourceChannel * source.bytesPerSample, source) } else { 0 } + if (applyGain) { + s32top = ((s32top.toLong() * gainQ16) shr 16).toInt() + } writeSubslot(output, s32top, subslotBytes) } } diff --git a/app/src/main/java/com/theveloper/pixelplay/data/service/player/usb/UsbAudioSink.kt b/app/src/main/java/com/theveloper/pixelplay/data/service/player/usb/UsbAudioSink.kt index 1f44ed03e..c032c94a9 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/service/player/usb/UsbAudioSink.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/service/player/usb/UsbAudioSink.kt @@ -117,8 +117,13 @@ class UsbAudioSink( isFloat = encoding == PcmRepacker.Encoding.FLOAT ) - val format = FormatNegotiator.negotiate(source, session.capabilities) - ?: throw AudioSink.ConfigurationException("No negotiable DAC format", inputFormat) + val format = FormatNegotiator.negotiate( + source, + session.capabilities, + // No hardware volume → a software gain stage may attenuate; a deeper subslot + // keeps the attenuated bits (still bit-perfect while the gain sits at unity). + preferDeepestDepth = session.capabilities.volume == null + ) ?: throw AudioSink.ConfigurationException("No negotiable DAC format", inputFormat) val sameStream = negotiated == format && session.currentFormat == format if (!sameStream) { @@ -261,7 +266,9 @@ class UsbAudioSink( wireScratch.clear() if (keep > 0) wireScratch.put(pendingWire) PcmRepacker.repack( - stage, stageEncoding, stageChannels, candidate.channels, candidate.subslotBytes, wireScratch + stage, stageEncoding, stageChannels, candidate.channels, candidate.subslotBytes, + wireScratch, + gainQ16 = session.softwareGainQ16 ) wireScratch.flip() // Swap scratch and pending so the next round reuses the other buffer. diff --git a/app/src/main/java/com/theveloper/pixelplay/data/service/player/usb/UsbDeviceVolumePlayer.kt b/app/src/main/java/com/theveloper/pixelplay/data/service/player/usb/UsbDeviceVolumePlayer.kt new file mode 100644 index 000000000..c6971f6c8 --- /dev/null +++ b/app/src/main/java/com/theveloper/pixelplay/data/service/player/usb/UsbDeviceVolumePlayer.kt @@ -0,0 +1,65 @@ +package com.theveloper.pixelplay.data.service.player.usb + +import androidx.media3.common.DeviceInfo +import androidx.media3.common.ForwardingPlayer +import androidx.media3.common.Player +import androidx.media3.common.util.UnstableApi +import com.theveloper.pixelplay.data.usb.UsbExclusiveModeController + +/** + * Wraps the session player while USB exclusive mode is engaged so the phone's volume keys + * control the DAC instead of doing nothing (there is no AudioTrack in exclusive mode). + * + * Advertising [DeviceInfo.PLAYBACK_TYPE_REMOTE] makes the media session route volume-key + * presses to [setDeviceVolume]/[increaseDeviceVolume]/[decreaseDeviceVolume], which map to + * the DAC's hardware feature unit when it has one, or to the software gain stage otherwise. + * The wrapper is created fresh on every exclusive-mode transition (players are rebuilt), + * so the advertised DeviceInfo is always current. + */ +@UnstableApi +class UsbDeviceVolumePlayer( + player: Player, + private val controller: UsbExclusiveModeController +) : ForwardingPlayer(player) { + + private val remoteDeviceInfo = DeviceInfo.Builder(DeviceInfo.PLAYBACK_TYPE_REMOTE) + .setMinVolume(0) + .setMaxVolume(controller.deviceVolumeMaxSteps) + .build() + + override fun getDeviceInfo(): DeviceInfo = + if (controller.deviceVolumeAvailable()) remoteDeviceInfo else super.getDeviceInfo() + + override fun getDeviceVolume(): Int = controller.deviceVolumeSteps() + + override fun isDeviceMuted(): Boolean = + controller.deviceVolumeAvailable() && controller.deviceVolumeSteps() == 0 + + override fun setDeviceVolume(volume: Int, flags: Int) { + controller.setDeviceVolumeSteps(volume) + } + + @Deprecated("Deprecated in Player") + override fun setDeviceVolume(volume: Int) = setDeviceVolume(volume, 0) + + override fun increaseDeviceVolume(flags: Int) { + controller.adjustDeviceVolume(+1) + } + + @Deprecated("Deprecated in Player") + override fun increaseDeviceVolume() = increaseDeviceVolume(0) + + override fun decreaseDeviceVolume(flags: Int) { + controller.adjustDeviceVolume(-1) + } + + @Deprecated("Deprecated in Player") + override fun decreaseDeviceVolume() = decreaseDeviceVolume(0) + + override fun setDeviceMuted(muted: Boolean, flags: Int) { + controller.setDeviceMuted(muted) + } + + @Deprecated("Deprecated in Player") + override fun setDeviceMuted(muted: Boolean) = setDeviceMuted(muted, 0) +} diff --git a/app/src/main/java/com/theveloper/pixelplay/data/usb/UsbDeviceInfo.kt b/app/src/main/java/com/theveloper/pixelplay/data/usb/UsbDeviceInfo.kt index 1e320c7c4..b9d330610 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/usb/UsbDeviceInfo.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/usb/UsbDeviceInfo.kt @@ -39,8 +39,22 @@ internal fun usbDeviceKey(vendorId: Int, productId: Int, serialNumber: String?): @Serializable data class UsbRememberedDevice( val label: String, - val autoResume: Boolean = true -) + val autoResume: Boolean = true, + /** + * Software volume for DACs without a hardware volume control, in dB (−60..0). + * Defaults to a safe −30 dB so sensitive IEMs never get blasted at full scale + * on first connect. 0 dB = unity = bit-perfect. + */ + val softwareVolumeDb: Float = DEFAULT_SOFTWARE_VOLUME_DB +) { + companion object { + const val DEFAULT_SOFTWARE_VOLUME_DB = -30f + const val MIN_SOFTWARE_VOLUME_DB = -60f + const val MAX_SOFTWARE_VOLUME_DB = 0f + /** Without the loudness acknowledgement, software volume is capped here. */ + const val UNACKNOWLEDGED_CAP_DB = -10f + } +} sealed interface UsbPermissionResult { val device: UsbDeviceInfo diff --git a/app/src/main/java/com/theveloper/pixelplay/data/usb/UsbExclusiveModeController.kt b/app/src/main/java/com/theveloper/pixelplay/data/usb/UsbExclusiveModeController.kt index e73bba37d..98e5fa8f2 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/usb/UsbExclusiveModeController.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/usb/UsbExclusiveModeController.kt @@ -24,6 +24,7 @@ import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -57,8 +58,14 @@ sealed interface UsbExclusiveState { val format: NegotiatedFormat, val source: SourceFormat, val conversion: Conversion, - val hardwareVolume: Boolean - ) : UsbExclusiveState + val hardwareVolume: Boolean, + /** Software volume in dB when the gain stage is engaged; null = unity path only. */ + val softwareVolumeDb: Float? = null + ) : UsbExclusiveState { + /** True bit-perfect: no format conversion AND no software attenuation. */ + val isBitPerfectOutput: Boolean + get() = conversion.isBitPerfect && (softwareVolumeDb == null || softwareVolumeDb >= -0.01f) + } /** Something failed; [recoverable] states can be retried from the UI. */ data class Error(val message: String, val recoverable: Boolean) : UsbExclusiveState @@ -89,10 +96,20 @@ class UsbExclusiveModeController @Inject constructor( var activeSession: UsbAudioSession? = null private set + /** Software volume (dB) while the gain stage is engaged for the current session, else null. */ + private val _softwareVolumeDb = MutableStateFlow(null) + val softwareVolumeDb: StateFlow = _softwareVolumeDb.asStateFlow() + private val mutex = Mutex() private var sessionDevice: UsbDeviceInfo? = null private var deniedDeviceKeys = mutableSetOf() private var requestedDeviceKeys = mutableSetOf() + private var fixedVolumeOutput = false + private var maxVolumeAcknowledged = false + /** Last known hardware volume fraction, cached so volume keys never block on USB I/O. */ + @Volatile + private var cachedHardwareFraction: Float = 0.5f + private var preMuteSoftwareDb: Float? = null init { scope.launch { @@ -105,6 +122,18 @@ class UsbExclusiveModeController @Inject constructor( scope.launch { usbDeviceManager.permissionEvents.collect { result -> onPermissionResult(result) } } + scope.launch { + userPreferencesRepository.usbExclusiveMaxVolumeAckFlow.collect { acknowledged -> + maxVolumeAcknowledged = acknowledged + } + } + scope.launch { + userPreferencesRepository.usbFixedVolumeOutputFlow.collect { fixed -> + fixedVolumeOutput = fixed + // Live-apply to the running session: purist mode drops the gain stage. + applySoftwareVolumeForCurrentSession() + } + } } /** Called from the settings UI to retry after a denial. */ @@ -136,7 +165,8 @@ class UsbExclusiveModeController @Inject constructor( format = format, source = source, conversion = format.conversion, - hardwareVolume = session.capabilities.volume != null + hardwareVolume = session.capabilities.volume != null, + softwareVolumeDb = _softwareVolumeDb.value ) } else { UsbExclusiveState.Ready(device, session.capabilities) @@ -161,9 +191,12 @@ class UsbExclusiveModeController @Inject constructor( val session = activeSession ?: return false val range = session.volumeRangeDb256() ?: return false val (min, max, resolution) = Triple(range[0], range[1], range[2].coerceAtLeast(1)) - val target = min + ((max - min) * fraction.coerceIn(0f, 1f)).toInt() + val clamped = fraction.coerceIn(0f, 1f) + val target = min + ((max - min) * clamped).toInt() val stepped = min + ((target - min) / resolution) * resolution - return session.setVolumeDb256(stepped) + return session.setVolumeDb256(stepped).also { ok -> + if (ok) cachedHardwareFraction = clamped + } } /** The DAC's current hardware volume as a 0..1 fraction of its range, or null. */ @@ -174,8 +207,145 @@ class UsbExclusiveModeController @Inject constructor( val (min, max) = range[0] to range[1] if (max <= min) return null return ((current - min).toFloat() / (max - min)).coerceIn(0f, 1f) + .also { cachedHardwareFraction = it } + } + + // ─── Software volume (DACs without a hardware feature unit) ──────────────── + + /** + * Sets the software volume, clamped to the allowed range — and, until the user has + * acknowledged the loudness warning, additionally capped at + * [UsbRememberedDevice.UNACKNOWLEDGED_CAP_DB]. Returns the dB actually applied, + * or null when the gain stage is not in play (hardware volume / fixed output). + */ + fun setSoftwareVolumeDb(db: Float): Float? { + val session = activeSession ?: return null + if (session.capabilities.volume != null || fixedVolumeOutput) return null + + val cap = if (maxVolumeAcknowledged) { + UsbRememberedDevice.MAX_SOFTWARE_VOLUME_DB + } else { + UsbRememberedDevice.UNACKNOWLEDGED_CAP_DB + } + val applied = db.coerceIn(UsbRememberedDevice.MIN_SOFTWARE_VOLUME_DB, cap) + preMuteSoftwareDb = null + session.softwareGainQ16 = gainQ16ForDb(applied) + _softwareVolumeDb.value = applied + _state.update { state -> + if (state is UsbExclusiveState.Active) state.copy(softwareVolumeDb = applied) else state + } + sessionDevice?.let { device -> + scope.launch { + val remembered = userPreferencesRepository.usbRememberedDevicesFlow.first()[device.key] + userPreferencesRepository.rememberUsbDevice( + device.key, + (remembered ?: UsbRememberedDevice(label = device.displayName)) + .copy(softwareVolumeDb = applied) + ) + } + } + return applied } + /** Installs (or removes) the gain stage for the current session per device/prefs. */ + private suspend fun applySoftwareVolumeForCurrentSession() { + mutex.withLock { applySoftwareVolumeLocked() } + } + + private suspend fun applySoftwareVolumeLocked() { + run { + val session = activeSession ?: return + val device = sessionDevice ?: return + if (session.capabilities.volume != null || fixedVolumeOutput) { + session.softwareGainQ16 = UsbAudioSession.UNITY_GAIN_Q16 + _softwareVolumeDb.value = null + _state.update { state -> + if (state is UsbExclusiveState.Active) state.copy(softwareVolumeDb = null) else state + } + return + } + val rememberedDb = userPreferencesRepository.usbRememberedDevicesFlow.first()[device.key] + ?.softwareVolumeDb ?: UsbRememberedDevice.DEFAULT_SOFTWARE_VOLUME_DB + val cap = if (maxVolumeAcknowledged) { + UsbRememberedDevice.MAX_SOFTWARE_VOLUME_DB + } else { + UsbRememberedDevice.UNACKNOWLEDGED_CAP_DB + } + val applied = rememberedDb.coerceIn(UsbRememberedDevice.MIN_SOFTWARE_VOLUME_DB, cap) + session.softwareGainQ16 = gainQ16ForDb(applied) + _softwareVolumeDb.value = applied + _state.update { state -> + if (state is UsbExclusiveState.Active) state.copy(softwareVolumeDb = applied) else state + } + Timber.tag(TAG).i("Software volume engaged at %.0f dB for %s", applied, device.key) + } + } + + + // ─── Device-volume mapping (phone volume keys via the media session) ────── + + /** Total steps advertised to the media session's device-volume surface. */ + val deviceVolumeMaxSteps: Int get() = DEVICE_VOLUME_STEPS + + /** True when volume keys have something to control (hw feature unit or software gain). */ + fun deviceVolumeAvailable(): Boolean { + val session = activeSession ?: return false + return session.capabilities.volume != null || + (!fixedVolumeOutput && _softwareVolumeDb.value != null) + } + + fun deviceVolumeSteps(): Int { + val session = activeSession ?: return 0 + return if (session.capabilities.volume != null) { + (cachedHardwareFraction * DEVICE_VOLUME_STEPS).toInt().coerceIn(0, DEVICE_VOLUME_STEPS) + } else { + val db = _softwareVolumeDb.value ?: return 0 + stepsForDb(db) + } + } + + fun setDeviceVolumeSteps(steps: Int) { + val session = activeSession ?: return + val clamped = steps.coerceIn(0, DEVICE_VOLUME_STEPS) + if (session.capabilities.volume != null) { + scope.launch(Dispatchers.IO) { + setHardwareVolume(clamped.toFloat() / DEVICE_VOLUME_STEPS) + } + } else { + setSoftwareVolumeDb(dbForSteps(clamped)) + } + } + + fun adjustDeviceVolume(delta: Int) = setDeviceVolumeSteps(deviceVolumeSteps() + delta) + + fun setDeviceMuted(muted: Boolean) { + val session = activeSession ?: return + if (session.capabilities.volume != null) { + scope.launch(Dispatchers.IO) { session.setMute(muted) } + } else if (muted) { + preMuteSoftwareDb = _softwareVolumeDb.value + setSoftwareVolumeDb(UsbRememberedDevice.MIN_SOFTWARE_VOLUME_DB) + } else { + setSoftwareVolumeDb(preMuteSoftwareDb ?: UsbRememberedDevice.DEFAULT_SOFTWARE_VOLUME_DB) + } + } + + private fun gainQ16ForDb(db: Float): Int = when { + db >= -0.01f -> UsbAudioSession.UNITY_GAIN_Q16 + db <= UsbRememberedDevice.MIN_SOFTWARE_VOLUME_DB -> 0 // floor = mute + else -> (Math.pow(10.0, db / 20.0) * UsbAudioSession.UNITY_GAIN_Q16).toInt() + } + + private fun stepsForDb(db: Float): Int = + (((db - UsbRememberedDevice.MIN_SOFTWARE_VOLUME_DB) / + (UsbRememberedDevice.MAX_SOFTWARE_VOLUME_DB - UsbRememberedDevice.MIN_SOFTWARE_VOLUME_DB)) * + DEVICE_VOLUME_STEPS).toInt().coerceIn(0, DEVICE_VOLUME_STEPS) + + private fun dbForSteps(steps: Int): Float = + UsbRememberedDevice.MIN_SOFTWARE_VOLUME_DB + + (UsbRememberedDevice.MAX_SOFTWARE_VOLUME_DB - UsbRememberedDevice.MIN_SOFTWARE_VOLUME_DB) * + steps.toFloat() / DEVICE_VOLUME_STEPS + private suspend fun reconcile(enabled: Boolean, devices: List) { mutex.withLock { if (!enabled) { @@ -314,10 +484,18 @@ class UsbExclusiveModeController @Inject constructor( activeSession = opened.session sessionDevice = device _state.value = UsbExclusiveState.Ready(device, opened.capabilities) + if (opened.capabilities.volume != null) { + // Prime the cache so volume keys have a truthful starting point. + hardwareVolumeFraction() + } else { + // Volume-less DAC: engage the software gain stage at the remembered + // (safe-by-default) level before any audio can flow. + applySoftwareVolumeLocked() + } Timber.tag(TAG).i( - "USB exclusive ready: %s (%s, rates=%s)", + "USB exclusive ready: %s (%s, rates=%s, swVolume=%s)", device.displayName, opened.capabilities.version, - opened.capabilities.allSampleRatesHz + opened.capabilities.allSampleRatesHz, _softwareVolumeDb.value ) } @@ -333,6 +511,8 @@ class UsbExclusiveModeController @Inject constructor( val device = sessionDevice activeSession = null sessionDevice = null + _softwareVolumeDb.value = null + preMuteSoftwareDb = null withContext(Dispatchers.IO) { runCatching { session.close() } } if (lost && device != null) { _sessionLost.tryEmit(device) @@ -357,6 +537,7 @@ class UsbExclusiveModeController @Inject constructor( private companion object { private const val TAG = "UsbExclusiveMode" + private const val DEVICE_VOLUME_STEPS = 30 } } diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/UsbAudioSettingsScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/UsbAudioSettingsScreen.kt index 79f86d26b..c620e23eb 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/UsbAudioSettingsScreen.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/UsbAudioSettingsScreen.kt @@ -180,12 +180,18 @@ fun UsbAudioSettingsScreen( item(key = "usb_volume_section") { val hardwareVolume by viewModel.hardwareVolumeFraction.collectAsStateWithLifecycle() + val softwareVolume by viewModel.softwareVolumeDb.collectAsStateWithLifecycle() + val fixedOutput by viewModel.fixedVolumeOutput.collectAsStateWithLifecycle() UsbVolumeSection( capabilities = capabilities, initialVolumeFraction = hardwareVolume, + softwareVolumeDb = softwareVolume, + fixedVolumeOutput = fixedOutput, maxVolumeAcknowledged = uiState.maxVolumeAcknowledged, onAcknowledge = viewModel::acknowledgeMaxVolume, - onVolumeChange = viewModel::setHardwareVolume + onVolumeChange = viewModel::setHardwareVolume, + onSoftwareVolumeChange = viewModel::setSoftwareVolumeDb, + onFixedOutputChange = viewModel::setFixedVolumeOutput ) } @@ -339,9 +345,13 @@ private fun UsbDeviceCard( private fun UsbVolumeSection( capabilities: UacCapabilities, initialVolumeFraction: Float?, + softwareVolumeDb: Float?, + fixedVolumeOutput: Boolean, maxVolumeAcknowledged: Boolean, onAcknowledge: () -> Unit, - onVolumeChange: (Float) -> Unit + onVolumeChange: (Float) -> Unit, + onSoftwareVolumeChange: (Float) -> Unit, + onFixedOutputChange: (Boolean) -> Unit ) { SettingsSection( title = stringResource(R.string.settings_usb_volume_section), @@ -370,42 +380,112 @@ private fun UsbVolumeSection( ) } } - } else { - Surface( - color = if (maxVolumeAcknowledged) MaterialTheme.colorScheme.surfaceContainer - else MaterialTheme.colorScheme.errorContainer, - shape = RoundedCornerShape(16.dp), - modifier = Modifier.fillMaxWidth() - ) { - Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { - Row(verticalAlignment = Alignment.CenterVertically) { - Icon( - Icons.Rounded.Warning, - null, - tint = if (maxVolumeAcknowledged) MaterialTheme.colorScheme.onSurfaceVariant - else MaterialTheme.colorScheme.onErrorContainer + return@SettingsSection + } + + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + if (!fixedVolumeOutput) { + // Software gain stage (the DAC has no hardware volume). 0 dB = bit-perfect. + var sliderDb by remember(softwareVolumeDb) { + mutableFloatStateOf(softwareVolumeDb ?: -30f) + } + Surface( + color = MaterialTheme.colorScheme.surfaceContainer, + shape = RoundedCornerShape(16.dp), + modifier = Modifier.fillMaxWidth() + ) { + Column(modifier = Modifier.padding(16.dp)) { + Row(modifier = Modifier.fillMaxWidth()) { + Text( + text = stringResource(R.string.settings_usb_sw_volume_title), + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.onSurface + ) + Spacer(Modifier.weight(1f)) + Text( + text = if (sliderDb >= -0.5f) { + stringResource(R.string.settings_usb_sw_volume_bit_perfect) + } else { + String.format(Locale.US, "%.0f dB", sliderDb) + }, + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.primary + ) + } + Slider( + value = sliderDb, + valueRange = -60f..0f, + onValueChange = { sliderDb = it }, + onValueChangeFinished = { onSoftwareVolumeChange(sliderDb) } ) - Spacer(Modifier.padding(horizontal = 6.dp)) Text( - text = stringResource(R.string.settings_usb_max_volume_warning_title), - style = MaterialTheme.typography.titleSmall, - color = if (maxVolumeAcknowledged) MaterialTheme.colorScheme.onSurface - else MaterialTheme.colorScheme.onErrorContainer + text = stringResource(R.string.settings_usb_sw_volume_subtitle), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant ) } - Text( - text = stringResource(R.string.settings_usb_max_volume_warning_text), - style = MaterialTheme.typography.bodySmall, - color = if (maxVolumeAcknowledged) MaterialTheme.colorScheme.onSurfaceVariant - else MaterialTheme.colorScheme.onErrorContainer - ) - if (!maxVolumeAcknowledged) { - OutlinedButton(onClick = onAcknowledge) { - Text(stringResource(R.string.settings_usb_max_volume_ack)) + } + if (!maxVolumeAcknowledged) { + Surface( + color = MaterialTheme.colorScheme.errorContainer, + shape = RoundedCornerShape(16.dp), + modifier = Modifier.fillMaxWidth() + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon(Icons.Rounded.Warning, null, tint = MaterialTheme.colorScheme.onErrorContainer) + Spacer(Modifier.padding(horizontal = 6.dp)) + Text( + text = stringResource(R.string.settings_usb_volume_cap_title), + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.onErrorContainer + ) + } + Text( + text = stringResource(R.string.settings_usb_volume_cap_text), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onErrorContainer + ) + OutlinedButton(onClick = onAcknowledge) { + Text(stringResource(R.string.settings_usb_volume_cap_unlock)) + } } } } + } else { + Surface( + color = MaterialTheme.colorScheme.errorContainer, + shape = RoundedCornerShape(16.dp), + modifier = Modifier.fillMaxWidth() + ) { + Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon(Icons.Rounded.Warning, null, tint = MaterialTheme.colorScheme.onErrorContainer) + Spacer(Modifier.padding(horizontal = 6.dp)) + Text( + text = stringResource(R.string.settings_usb_max_volume_warning_title), + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.onErrorContainer + ) + } + Text( + text = stringResource(R.string.settings_usb_max_volume_warning_text), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onErrorContainer + ) + } + } } + SwitchSettingItem( + title = stringResource(R.string.settings_usb_fixed_output_title), + subtitle = stringResource(R.string.settings_usb_fixed_output_subtitle), + checked = fixedVolumeOutput, + onCheckedChange = onFixedOutputChange, + leadingIcon = { Icon(Icons.Rounded.VolumeUp, null, tint = MaterialTheme.colorScheme.secondary) } + ) } } } diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt index eebf0a59e..31955b405 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt @@ -1250,11 +1250,18 @@ class PlayerViewModel @Inject constructor( if (hz % 1000 == 0) "${hz / 1000}kHz" else String.format(java.util.Locale.US, "%.1fkHz", hz / 1000.0) val bits = active.format.candidate.bitResolution - return if (active.conversion.isBitPerfect) { + val base = if (active.conversion.isBitPerfect) { "USB • ${bits}bit/${kHz(active.format.sampleRateHz)}" } else { "USB • ${bits}bit/${kHz(active.source.sampleRateHz)}→${kHz(active.format.sampleRateHz)}" } + // Software attenuation is not bit-perfect, so the badge must say so. + val softDb = active.softwareVolumeDb + return if (softDb != null && softDb < -0.05f) { + base + String.format(java.util.Locale.US, " • %.0fdB", softDb) + } else { + base + } } private val usbOutputLabelFlow = usbExclusiveModeController.state diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/UsbAudioSettingsViewModel.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/UsbAudioSettingsViewModel.kt index f9f5dacbe..3b2efc8da 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/UsbAudioSettingsViewModel.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/UsbAudioSettingsViewModel.kt @@ -90,6 +90,20 @@ class UsbAudioSettingsViewModel @Inject constructor( .distinctUntilChanged() .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null) + /** Software volume (dB) for volume-less DACs, null when the gain stage is off. */ + val softwareVolumeDb: StateFlow = controller.softwareVolumeDb + + val fixedVolumeOutput: StateFlow = userPreferencesRepository.usbFixedVolumeOutputFlow + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false) + + fun setSoftwareVolumeDb(db: Float) { + controller.setSoftwareVolumeDb(db) + } + + fun setFixedVolumeOutput(fixed: Boolean) { + viewModelScope.launch { userPreferencesRepository.setUsbFixedVolumeOutput(fixed) } + } + // ─── Debug: raw driver bring-up tone (bypasses ExoPlayer entirely) ──────── private var toneJob: Job? = null diff --git a/app/src/main/res/values-es/strings_settings.xml b/app/src/main/res/values-es/strings_settings.xml index 9413b079d..8a3b6a0de 100644 --- a/app/src/main/res/values-es/strings_settings.xml +++ b/app/src/main/res/values-es/strings_settings.xml @@ -674,4 +674,12 @@ Entendido Reproducir tono de prueba Reintentar + Volumen por software + Bit-perfect + Este DAC no tiene control de volumen, así que PixelPlay atenúa la señal antes de enviarla. A 0 dB la señal es bit-perfect pero suena a nivel de línea completo. + Volumen limitado por seguridad + Los niveles por encima de −10 dB están bloqueados hasta que confirmes que tus auriculares o amplificador soportan la salida a nivel de línea completo. La escala completa en IEM sensibles puede ser peligrosamente alta. + Entiendo, desbloquear volumen completo + Salida fija a nivel de línea + Omite el volumen por software y emite siempre a escala completa — para alimentar un amplificador externo diff --git a/app/src/main/res/values-fr/strings_settings.xml b/app/src/main/res/values-fr/strings_settings.xml index 349045014..a572a225d 100644 --- a/app/src/main/res/values-fr/strings_settings.xml +++ b/app/src/main/res/values-fr/strings_settings.xml @@ -670,4 +670,12 @@ Compris Jouer une tonalité de test Réessayer + Volume logiciel + Bit-perfect + Ce DAC n’a pas de contrôle de volume, PixelPlay atténue donc le signal avant de l’envoyer. À 0 dB le flux est bit-perfect mais joue au niveau ligne maximal. + Volume plafonné par sécurité + Les niveaux au-dessus de −10 dB sont verrouillés tant que vous n’avez pas confirmé que votre casque ou amplificateur supporte une sortie niveau ligne maximale. La pleine échelle dans des IEM sensibles peut être dangereusement forte. + Je comprends, déverrouiller le volume complet + Sortie fixe niveau ligne + Contourne le volume logiciel et sort toujours à pleine échelle — pour alimenter un amplificateur externe diff --git a/app/src/main/res/values/strings_settings.xml b/app/src/main/res/values/strings_settings.xml index bccb69b31..eafc95f6d 100644 --- a/app/src/main/res/values/strings_settings.xml +++ b/app/src/main/res/values/strings_settings.xml @@ -674,4 +674,12 @@ Got it Play test tone Retry + Software volume + Bit-perfect + This DAC has no volume control, so PixelPlay attenuates the signal before sending it. At 0 dB the stream is bit-perfect but plays at full line level. + Volume capped for safety + Levels above −10 dB are locked until you confirm your headphones or amplifier can handle full line-level output. Full scale into sensitive IEMs can be dangerously loud. + I understand, unlock full volume + Fixed line-level output + Bypass the software volume entirely and always output at full scale — for feeding an external amplifier diff --git a/app/src/test/java/com/theveloper/pixelplay/data/service/player/usb/PcmRepackerTest.kt b/app/src/test/java/com/theveloper/pixelplay/data/service/player/usb/PcmRepackerTest.kt index 0f5ce96c0..86dbfdbb0 100644 --- a/app/src/test/java/com/theveloper/pixelplay/data/service/player/usb/PcmRepackerTest.kt +++ b/app/src/test/java/com/theveloper/pixelplay/data/service/player/usb/PcmRepackerTest.kt @@ -116,4 +116,82 @@ class PcmRepackerTest { PcmRepacker.repack(input, PcmRepacker.Encoding.PCM_16, 1, 1, 2, out(2)) assertThat(input.hasRemaining()).isFalse() } + + // ─── Software gain stage ────────────────────────────────────────────────── + + @Test + fun `unity gain is byte-identical to the no-gain path`() { + val plain = out(4) + PcmRepacker.repack(buffer(0x34, 0x12, 0xDC, 0xFE), PcmRepacker.Encoding.PCM_16, 2, 2, 2, plain) + val unity = out(4) + PcmRepacker.repack( + buffer(0x34, 0x12, 0xDC, 0xFE), PcmRepacker.Encoding.PCM_16, 2, 2, 2, unity, + gainQ16 = PcmRepacker.UNITY_GAIN_Q16 + ) + assertThat(unity.bytes()).isEqualTo(plain.bytes()) + } + + @Test + fun `half gain halves samples exactly in the 32-bit domain`() { + // s16 0x4000 (s32top 0x40000000) at gain 0.5 → 0x20000000 → 16-bit wire 0x2000 + val output = out(2) + PcmRepacker.repack( + buffer(0x00, 0x40), PcmRepacker.Encoding.PCM_16, 1, 1, 2, output, + gainQ16 = PcmRepacker.UNITY_GAIN_Q16 / 2 + ) + assertThat(output.bytes()).containsExactly(0x00, 0x20).inOrder() + } + + @Test + fun `attenuated 16-bit material keeps fractional bits in a deeper subslot`() { + // Smallest s16 step (0x0001 → s32top 0x00010000) halved is 0x00008000 — below the + // 16-bit floor but exactly representable in a 24-bit subslot (0x000080). + val output = out(3) + PcmRepacker.repack( + buffer(0x01, 0x00), PcmRepacker.Encoding.PCM_16, 1, 1, 3, output, + gainQ16 = PcmRepacker.UNITY_GAIN_Q16 / 2 + ) + assertThat(output.bytes()).containsExactly(0x80, 0x00, 0x00).inOrder() + } + + @Test + fun `zero gain silences the output`() { + val output = out(4) + PcmRepacker.repack( + buffer(0x34, 0x12, 0xDC, 0xFE), PcmRepacker.Encoding.PCM_16, 2, 2, 2, output, + gainQ16 = 0 + ) + assertThat(output.bytes()).containsExactly(0x00, 0x00, 0x00, 0x00).inOrder() + } + + @Test + fun `negative samples attenuate toward zero without wrapping`() { + // Most negative 16-bit sample halved: 0x8000 → s32top MIN_VALUE → ÷2 = 0xC0000000 + val output = out(2) + PcmRepacker.repack( + buffer(0x00, 0x80), PcmRepacker.Encoding.PCM_16, 1, 1, 2, output, + gainQ16 = PcmRepacker.UNITY_GAIN_Q16 / 2 + ) + assertThat(output.bytes()).containsExactly(0x00, 0xC0).inOrder() + } + + @Test + fun `gainQ16FromDb golden values`() { + assertThat(PcmRepacker.gainQ16FromDb(null)).isEqualTo(PcmRepacker.UNITY_GAIN_Q16) + assertThat(PcmRepacker.gainQ16FromDb(0f)).isEqualTo(PcmRepacker.UNITY_GAIN_Q16) + assertThat(PcmRepacker.gainQ16FromDb(-90f)).isEqualTo(0) + assertThat(PcmRepacker.gainQ16FromDb(-120f)).isEqualTo(0) + // −20 dB is exactly 0.1 linear; −6.02 dB is within one Q16 step of 0.5. + assertThat(PcmRepacker.gainQ16FromDb(-20f)).isEqualTo((0.1 * 65536).toInt()) + val half = PcmRepacker.gainQ16FromDb(-6.0206f) + assertThat(half).isAtLeast(32766) + assertThat(half).isAtMost(32769) + // Monotonic: deeper attenuation never yields a higher gain. + var previous = Int.MAX_VALUE + for (db in 0 downTo -90) { + val gain = PcmRepacker.gainQ16FromDb(db.toFloat()) + assertThat(gain).isAtMost(previous) + previous = gain + } + } } diff --git a/app/src/test/java/com/theveloper/pixelplay/data/service/player/usb/UsbAudioSinkTest.kt b/app/src/test/java/com/theveloper/pixelplay/data/service/player/usb/UsbAudioSinkTest.kt index eae265465..3992f45b6 100644 --- a/app/src/test/java/com/theveloper/pixelplay/data/service/player/usb/UsbAudioSinkTest.kt +++ b/app/src/test/java/com/theveloper/pixelplay/data/service/player/usb/UsbAudioSinkTest.kt @@ -10,6 +10,7 @@ import com.theveloper.pixelplay.usbaudio.descriptor.EndpointSyncType import com.theveloper.pixelplay.usbaudio.descriptor.FormatCandidate import com.theveloper.pixelplay.usbaudio.descriptor.UacCapabilities import com.theveloper.pixelplay.usbaudio.descriptor.UacVersion +import com.theveloper.pixelplay.usbaudio.descriptor.VolumeCapability import com.theveloper.pixelplay.usbaudio.negotiation.NegotiatedFormat import io.mockk.every import io.mockk.mockk @@ -40,7 +41,9 @@ class UsbAudioSinkTest { clockSourceId = 0x29, uac1SampleRateControl = false ) ), - volume = null + // Hardware volume present → the sink negotiates for minimal conversion (matching + // depth) instead of preferring the deepest subslot for the software gain stage. + volume = VolumeCapability(featureUnitId = 0x0A, hasMasterVolume = true, hasMasterMute = true) ) private lateinit var session: UsbAudioSession @@ -64,6 +67,7 @@ class UsbAudioSinkTest { session = mockk(relaxed = true) { every { capabilities } returns this@UsbAudioSinkTest.capabilities + every { softwareGainQ16 } returns UsbAudioSession.UNITY_GAIN_Q16 every { isAlive } answers { alive } every { currentFormat } answers { configuredFormat } every { configure(any(), any()) } answers { diff --git a/app/src/test/java/com/theveloper/pixelplay/data/usb/UsbExclusiveModeControllerTest.kt b/app/src/test/java/com/theveloper/pixelplay/data/usb/UsbExclusiveModeControllerTest.kt index fd6401848..bc376cd33 100644 --- a/app/src/test/java/com/theveloper/pixelplay/data/usb/UsbExclusiveModeControllerTest.kt +++ b/app/src/test/java/com/theveloper/pixelplay/data/usb/UsbExclusiveModeControllerTest.kt @@ -27,6 +27,8 @@ class UsbExclusiveModeControllerTest { private val attachedFlow = MutableStateFlow>(emptyList()) private val permissionFlow = MutableSharedFlow(extraBufferCapacity = 4) private val rememberedFlow = MutableStateFlow>(emptyMap()) + private val ackFlow = MutableStateFlow(false) + private val fixedOutputFlow = MutableStateFlow(false) private val requestedPermissions = mutableListOf() private var sessionsOpened = 0 @@ -41,6 +43,8 @@ class UsbExclusiveModeControllerTest { private val prefs: UserPreferencesRepository = mockk(relaxed = true) { every { usbExclusiveModeEnabledFlow } returns enabledFlow every { usbRememberedDevicesFlow } returns rememberedFlow + every { usbExclusiveMaxVolumeAckFlow } returns ackFlow + every { usbFixedVolumeOutputFlow } returns fixedOutputFlow coEvery { rememberUsbDevice(any(), any()) } answers { rememberedFlow.value = rememberedFlow.value + (firstArg() to secondArg()) @@ -49,12 +53,15 @@ class UsbExclusiveModeControllerTest { private var claimResult = true private var claimCount = 0 + private var appliedGainQ16 = UsbAudioSession.UNITY_GAIN_Q16 private val session: UsbAudioSession = mockk(relaxed = true) { every { isAlive } returns true every { capabilities } returns caps() every { claim(any(), any()) } answers { claimCount++; claimResult } every { close() } answers { sessionCloseCount++ } + every { softwareGainQ16 = any() } answers { appliedGainQ16 = firstArg() } + every { softwareGainQ16 } answers { appliedGainQ16 } } private fun caps() = UacCapabilities( @@ -274,4 +281,92 @@ class UsbExclusiveModeControllerTest { controller.onSinkFormatChanged(null, null) assertThat(controller.state.value).isInstanceOf(UsbExclusiveState.Ready::class.java) } + + // ─── Software volume (fixture DAC has no hardware feature unit) ─────────── + + private suspend fun openVolumelessSession(controller: UsbExclusiveModeController) { + enabledFlow.value = true + attachedFlow.value = listOf(device(permission = true)) + while (controller.softwareVolumeDb.value == null) kotlinx.coroutines.yield() + } + + @Test + fun `volume-less DAC engages the gain stage at the safe default before audio flows`() = runTest { + val controller = controller() + openVolumelessSession(controller) + + assertThat(controller.softwareVolumeDb.value) + .isEqualTo(UsbRememberedDevice.DEFAULT_SOFTWARE_VOLUME_DB) + val expectedGain = + (Math.pow(10.0, UsbRememberedDevice.DEFAULT_SOFTWARE_VOLUME_DB / 20.0) * + UsbAudioSession.UNITY_GAIN_Q16).toInt() + assertThat(appliedGainQ16).isEqualTo(expectedGain) + } + + @Test + fun `software volume is capped until the loudness warning is acknowledged`() = runTest { + val controller = controller() + openVolumelessSession(controller) + + // Asking for full scale only gets the unacknowledged cap… + assertThat(controller.setSoftwareVolumeDb(0f)) + .isEqualTo(UsbRememberedDevice.UNACKNOWLEDGED_CAP_DB) + assertThat(controller.softwareVolumeDb.value) + .isEqualTo(UsbRememberedDevice.UNACKNOWLEDGED_CAP_DB) + + // …until the acknowledgement lands, then 0 dB (bit-perfect) unlocks. + ackFlow.value = true + assertThat(controller.setSoftwareVolumeDb(0f)).isEqualTo(0f) + assertThat(appliedGainQ16).isEqualTo(UsbAudioSession.UNITY_GAIN_Q16) + } + + @Test + fun `software volume persists per device and is restored on the next session`() = runTest { + val dac = device(permission = true) + rememberedFlow.value = mapOf( + dac.key to UsbRememberedDevice(label = "Test DAC", softwareVolumeDb = -18f) + ) + val controller = controller() + openVolumelessSession(controller) + + // Remembered level restored instead of the -30 dB default. + assertThat(controller.softwareVolumeDb.value).isEqualTo(-18f) + + // Changing it writes back to the per-device record. + controller.setSoftwareVolumeDb(-24f) + while (rememberedFlow.value[dac.key]?.softwareVolumeDb != -24f) kotlinx.coroutines.yield() + assertThat(rememberedFlow.value[dac.key]!!.softwareVolumeDb).isEqualTo(-24f) + } + + @Test + fun `fixed output mode drops the gain stage to unity`() = runTest { + val controller = controller() + openVolumelessSession(controller) + + fixedOutputFlow.value = true + while (controller.softwareVolumeDb.value != null) kotlinx.coroutines.yield() + + assertThat(appliedGainQ16).isEqualTo(UsbAudioSession.UNITY_GAIN_Q16) + // The slider has nothing to control in purist mode. + assertThat(controller.setSoftwareVolumeDb(-20f)).isNull() + assertThat(controller.deviceVolumeAvailable()).isFalse() + } + + @Test + fun `volume keys map onto the software gain range`() = runTest { + val controller = controller() + openVolumelessSession(controller) + + assertThat(controller.deviceVolumeAvailable()).isTrue() + // −30 dB inside −60..0 is the midpoint of the 30-step scale. + assertThat(controller.deviceVolumeSteps()).isEqualTo(15) + + controller.adjustDeviceVolume(+1) + assertThat(controller.softwareVolumeDb.value).isEqualTo(-28f) + + controller.setDeviceVolumeSteps(0) + assertThat(controller.softwareVolumeDb.value) + .isEqualTo(UsbRememberedDevice.MIN_SOFTWARE_VOLUME_DB) + assertThat(appliedGainQ16).isEqualTo(0) // floor mutes outright + } } diff --git a/docs/usb-exclusive-mode.md b/docs/usb-exclusive-mode.md index 1ee8f462d..18d097567 100644 --- a/docs/usb-exclusive-mode.md +++ b/docs/usb-exclusive-mode.md @@ -63,10 +63,29 @@ output. The deck/preview player likewise stays on the phone output while a DAC i If the DAC exposes a UAC feature unit with a master volume, the USB Audio settings screen offers a **hardware volume** slider (SET CUR on the feature unit, in the DAC's own 1/256 dB -steps — the PCM samples are untouched). Otherwise the output is fixed at full scale and the -screen shows a one-time **line-level warning**. The phone's volume keys do not affect the -DAC in exclusive mode (there is no AudioTrack); mapping volume keys to the feature unit is -a possible follow-up via a Media3 device-volume override. +steps — the PCM samples are untouched). + +DACs **without** a hardware volume (dongles wired straight to sensitive IEMs) get a +**software gain stage** instead of raw full-scale output: + +- Attenuation happens in `PcmRepacker` as a single Q16 fixed-point multiply in the 32-bit + domain *before* subslot packing, and the negotiator prefers the deepest available subslot + (`preferDeepestDepth`) so attenuated 16-bit material keeps its low-order bits in a + 24/32-bit slot. At **0 dB the multiply is skipped entirely** — unity is the same + untouched code path as always, and only then does the UI (and the Now Playing badge) + say *bit-perfect*; at any other level the badge appends the attenuation (e.g. + `USB • 16bit/44.1kHz • −30dB`). +- First connect defaults to a safe **−30 dB**, and the slider is capped at **−10 dB** + until the user explicitly acknowledges the loudness warning. The chosen level is + remembered **per device**. +- A **Fixed line-level output** toggle bypasses the gain stage completely (purist mode / + feeding an external amplifier) and restores the old full-scale behaviour with its + warning card. + +The phone's **volume keys** control the DAC in exclusive mode: the media-session player is +wrapped in a `ForwardingPlayer` advertising `PLAYBACK_TYPE_REMOTE` device volume +(30 steps), which maps onto the hardware feature unit when present, else onto the software +gain range (−60…0 dB, where the floor mutes). ## Lifecycle & edge cases @@ -110,7 +129,10 @@ physical DAC (ideally one UAC2 async device with a rate display and one UAC1 don 8. Replug: permission re-grant (or remembered) → auto-resume if enabled. 9. Incoming call and alarm: playback pauses and resumes; no mixing into the DAC. 10. 1-hour screen-off playback: stream survives doze. -11. Hardware-volume DAC: slider changes loudness; fixed-volume DAC: warning shown. +11. Hardware-volume DAC: slider changes loudness; volume-less DAC: software slider engages + at −30 dB, unlock card gates levels above −10 dB, badge shows the attenuation. +12. Volume keys change DAC loudness while exclusive (both hardware and software paths); + with *Fixed line-level output* on, keys do nothing and output is full scale. ## Licensing diff --git a/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/UsbAudioSession.kt b/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/UsbAudioSession.kt index 8eba39311..ab36d57bb 100644 --- a/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/UsbAudioSession.kt +++ b/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/UsbAudioSession.kt @@ -37,6 +37,14 @@ class UsbAudioSession private constructor( this.capabilities = capabilities } + /** + * Software-volume gain in Q16 fixed point (65536 = unity/bit-perfect). Set by the + * exclusive-mode controller for DACs without a hardware volume feature unit; the + * audio sink reads it on every buffer. Unity means the gain stage is skipped entirely. + */ + @Volatile + var softwareGainQ16: Int = UNITY_GAIN_Q16 + private val lock = Any() private var closed = false private var claimed = false @@ -248,6 +256,7 @@ class UsbAudioSession private constructor( companion object { const val DEFAULT_RING_BUFFER_MS = 250 + const val UNITY_GAIN_Q16 = 1 shl 16 /** * Wraps an open, permission-granted connection. [capabilities] must come from diff --git a/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/negotiation/FormatNegotiator.kt b/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/negotiation/FormatNegotiator.kt index d8704bde7..a48f7d8f2 100644 --- a/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/negotiation/FormatNegotiator.kt +++ b/usbaudio/src/main/java/com/theveloper/pixelplay/usbaudio/negotiation/FormatNegotiator.kt @@ -45,7 +45,16 @@ data class NegotiatedFormat( */ object FormatNegotiator { - fun negotiate(source: SourceFormat, caps: UacCapabilities): NegotiatedFormat? { + /** + * [preferDeepestDepth]: choose the deepest adequate bit resolution instead of the + * tightest. Used for DACs without hardware volume, where a software gain stage may + * attenuate — a deeper subslot preserves the attenuated sample's low bits. + */ + fun negotiate( + source: SourceFormat, + caps: UacCapabilities, + preferDeepestDepth: Boolean = false + ): NegotiatedFormat? { return caps.formats .mapNotNull { candidate -> evaluate(source, candidate) } .minWithOrNull( @@ -53,8 +62,9 @@ object FormatNegotiator { { if (it.conversion.downmixed) 1 else 0 }, { if (it.conversion.resampled) 1 else 0 }, { if (it.conversion.depthReduced) 1 else 0 }, - // Tightest adequate depth (24-bit source → prefer 24 over 32)… - { depthOverhead(source, it.candidate) }, + // Tightest adequate depth (24-bit source → prefer 24 over 32) unless a + // software volume stage wants headroom — then deepest adequate. + { depthOverhead(source, it.candidate, preferDeepestDepth) }, // …then the least-destructive rate choice. { rateDistance(source.sampleRateHz, it.sampleRateHz) }, // Stable tiebreak. @@ -98,10 +108,15 @@ object FormatNegotiator { internal fun sameFamily(a: Int, b: Int): Boolean = (a % 11_025 == 0) == (b % 11_025 == 0) - private fun depthOverhead(source: SourceFormat, candidate: FormatCandidate): Int { + private fun depthOverhead( + source: SourceFormat, + candidate: FormatCandidate, + preferDeepestDepth: Boolean + ): Int { val overhead = candidate.bitResolution - source.effectiveBitDepth // Inadequate depth sorts after any adequate depth; among inadequate, deeper is better. - return if (overhead >= 0) overhead else 1000 - overhead + if (overhead < 0) return 1000 - overhead + return if (preferDeepestDepth) 64 - candidate.bitResolution else overhead } private fun rateDistance(sourceRate: Int, chosenRate: Int): Long { diff --git a/usbaudio/src/test/java/com/theveloper/pixelplay/usbaudio/negotiation/FormatNegotiatorTest.kt b/usbaudio/src/test/java/com/theveloper/pixelplay/usbaudio/negotiation/FormatNegotiatorTest.kt index 4c58a904c..3066e6601 100644 --- a/usbaudio/src/test/java/com/theveloper/pixelplay/usbaudio/negotiation/FormatNegotiatorTest.kt +++ b/usbaudio/src/test/java/com/theveloper/pixelplay/usbaudio/negotiation/FormatNegotiatorTest.kt @@ -127,6 +127,33 @@ class FormatNegotiatorTest { assertThat(result.conversion.isBitPerfect).isTrue() } + @Test + fun `preferDeepestDepth picks the deepest adequate alt setting`() { + // Volume-less DAC: the software gain stage wants subslot headroom, so a 16-bit + // source should land on the 32-bit alt instead of the byte-exact 16-bit one. + val result = FormatNegotiator.negotiate( + SourceFormat(44_100, 16, 2, isFloat = false), typicalDac, preferDeepestDepth = true + )!! + assertThat(result.candidate.bitResolution).isEqualTo(32) + assertThat(result.sampleRateHz).isEqualTo(44_100) + // Widening the subslot is lossless — still bit-perfect at unity gain. + assertThat(result.conversion.isBitPerfect).isTrue() + } + + @Test + fun `preferDeepestDepth never trades an exact rate for depth`() { + val dac = caps( + candidate(altSetting = 1, bitResolution = 16, rates = listOf(44_100)), + candidate(altSetting = 2, bitResolution = 32, subslotBytes = 4, rates = listOf(48_000)) + ) + val result = FormatNegotiator.negotiate( + SourceFormat(44_100, 16, 2, isFloat = false), dac, preferDeepestDepth = true + )!! + // Resampling outranks depth preference: stay at 44.1 kHz on the 16-bit alt. + assertThat(result.candidate.altSetting).isEqualTo(1) + assertThat(result.conversion.resampled).isFalse() + } + @Test fun `chooseRate helper table`() { val rates = listOf(44_100, 48_000, 88_200, 96_000) From 30aac3409d6fe8535050a629e8ac0d755497f70c Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 8 Jul 2026 21:44:22 +0000 Subject: [PATCH 15/17] ci: cut usb beta release v0.7.5-usb-beta.2 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FZMSPh3TgbRAMqgW4q1p6V --- .github/release-tag.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/release-tag.txt b/.github/release-tag.txt index 32eef0351..3ba855b96 100644 --- a/.github/release-tag.txt +++ b/.github/release-tag.txt @@ -1 +1 @@ -v0.7.5-usb-beta.1 +v0.7.5-usb-beta.2 From 39fd7a0877b992057408acbd45a3855e7e0277f6 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 9 Jul 2026 07:31:01 +0000 Subject: [PATCH 16/17] fix(usb): advertise device-volume commands so volume keys reach the DAC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The wrapped ExoPlayer is built without setDeviceVolumeControlEnabled(true), so it never advertises COMMAND_GET/SET/ADJUST_DEVICE_VOLUME. The media session downgrades a remote-playback player without those commands to VOLUME_CONTROL_FIXED and reports its volume as 0 — the DeviceInfo override alone left the volume keys dead. UsbDeviceVolumePlayer now augments getAvailableCommands()/isCommandAvailable() with the three commands its overrides actually implement. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FZMSPh3TgbRAMqgW4q1p6V --- .../player/usb/UsbDeviceVolumePlayer.kt | 19 +++++ .../player/usb/UsbDeviceVolumePlayerTest.kt | 83 +++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 app/src/test/java/com/theveloper/pixelplay/data/service/player/usb/UsbDeviceVolumePlayerTest.kt diff --git a/app/src/main/java/com/theveloper/pixelplay/data/service/player/usb/UsbDeviceVolumePlayer.kt b/app/src/main/java/com/theveloper/pixelplay/data/service/player/usb/UsbDeviceVolumePlayer.kt index c6971f6c8..a16a695a2 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/service/player/usb/UsbDeviceVolumePlayer.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/service/player/usb/UsbDeviceVolumePlayer.kt @@ -27,6 +27,25 @@ class UsbDeviceVolumePlayer( .setMaxVolume(controller.deviceVolumeMaxSteps) .build() + /** + * The wrapped ExoPlayer is built without `setDeviceVolumeControlEnabled(true)`, so it + * does not advertise the device-volume commands — and the media session downgrades a + * remote device without them to VOLUME_CONTROL_FIXED (volume keys ignored). Advertise + * them here; the overrides below are the implementation. + */ + override fun getAvailableCommands(): Player.Commands = + super.getAvailableCommands() + .buildUpon() + .addAll( + Player.COMMAND_GET_DEVICE_VOLUME, + Player.COMMAND_SET_DEVICE_VOLUME_WITH_FLAGS, + Player.COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS + ) + .build() + + override fun isCommandAvailable(command: Int): Boolean = + availableCommands.contains(command) + override fun getDeviceInfo(): DeviceInfo = if (controller.deviceVolumeAvailable()) remoteDeviceInfo else super.getDeviceInfo() diff --git a/app/src/test/java/com/theveloper/pixelplay/data/service/player/usb/UsbDeviceVolumePlayerTest.kt b/app/src/test/java/com/theveloper/pixelplay/data/service/player/usb/UsbDeviceVolumePlayerTest.kt new file mode 100644 index 000000000..f953b5aab --- /dev/null +++ b/app/src/test/java/com/theveloper/pixelplay/data/service/player/usb/UsbDeviceVolumePlayerTest.kt @@ -0,0 +1,83 @@ +package com.theveloper.pixelplay.data.service.player.usb + +import androidx.media3.common.DeviceInfo +import androidx.media3.common.Player +import com.google.common.truth.Truth.assertThat +import com.theveloper.pixelplay.data.usb.UsbExclusiveModeController +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.junit.jupiter.api.Test + +class UsbDeviceVolumePlayerTest { + + private val inner: Player = mockk(relaxed = true) { + every { availableCommands } returns Player.Commands.EMPTY + every { deviceInfo } returns DeviceInfo.UNKNOWN + } + + private var volumeAvailable = true + private val controller: UsbExclusiveModeController = mockk(relaxed = true) { + every { deviceVolumeMaxSteps } returns 30 + every { deviceVolumeAvailable() } answers { volumeAvailable } + every { deviceVolumeSteps() } returns 15 + } + + private fun player() = UsbDeviceVolumePlayer(inner, controller) + + @Test + fun `advertises the device-volume commands the inner player lacks`() { + // Without these the media session downgrades to VOLUME_CONTROL_FIXED and the + // phone's volume keys never reach the DAC. Player.Commands is inert under the + // mockable android jar (SparseBooleanArray stubs), so assert the builder wiring. + val added = mutableListOf() + val built: Player.Commands = mockk() + val builder: Player.Commands.Builder = mockk { + every { addAll(*anyIntVararg()) } answers { + added += (invocation.args.first() as IntArray).toList() + self as Player.Commands.Builder + } + every { build() } returns built + } + val innerCommands: Player.Commands = mockk { every { buildUpon() } returns builder } + every { inner.availableCommands } returns innerCommands + + assertThat(player().availableCommands).isSameInstanceAs(built) + assertThat(added).containsExactly( + Player.COMMAND_GET_DEVICE_VOLUME, + Player.COMMAND_SET_DEVICE_VOLUME_WITH_FLAGS, + Player.COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS + ) + + // isCommandAvailable consults the augmented set, not the inner player. + every { built.contains(Player.COMMAND_GET_DEVICE_VOLUME) } returns true + assertThat(player().isCommandAvailable(Player.COMMAND_GET_DEVICE_VOLUME)).isTrue() + } + + @Test + fun `reports a remote device sized to the controller step scale`() { + val info = player().deviceInfo + assertThat(info.playbackType).isEqualTo(DeviceInfo.PLAYBACK_TYPE_REMOTE) + assertThat(info.maxVolume).isEqualTo(30) + assertThat(player().deviceVolume).isEqualTo(15) + } + + @Test + fun `falls back to the inner player's device info when volume is unavailable`() { + volumeAvailable = false // fixed line-level output + assertThat(player().deviceInfo).isEqualTo(DeviceInfo.UNKNOWN) + } + + @Test + fun `volume operations route to the controller`() { + val player = player() + player.setDeviceVolume(20, 0) + player.increaseDeviceVolume(0) + player.decreaseDeviceVolume(0) + player.setDeviceMuted(true, 0) + verify { controller.setDeviceVolumeSteps(20) } + verify { controller.adjustDeviceVolume(1) } + verify { controller.adjustDeviceVolume(-1) } + verify { controller.setDeviceMuted(true) } + } +} From 021ec3d7f9dbe084016aa7b369d0766f741ba1c3 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 9 Jul 2026 07:31:02 +0000 Subject: [PATCH 17/17] ci: cut usb beta release v0.7.5-usb-beta.3 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FZMSPh3TgbRAMqgW4q1p6V --- .github/release-tag.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/release-tag.txt b/.github/release-tag.txt index 3ba855b96..1247116b4 100644 --- a/.github/release-tag.txt +++ b/.github/release-tag.txt @@ -1 +1 @@ -v0.7.5-usb-beta.2 +v0.7.5-usb-beta.3