diff --git a/.github/ISSUE_TEMPLATE/bug-report--.md b/.github/ISSUE_TEMPLATE/bug-report--.md new file mode 100644 index 00000000..e74e7d0d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report--.md @@ -0,0 +1,22 @@ +--- +name: "Bug report \U0001F41E" +about: Create a bug report +labels: bug + +--- + +## Describe the bug +A clear and concise description of what the bug is. + +### Steps to reproduce +Steps to reproduce the behavior. + +### Expected behavior +A clear and concise description of what you expected to happen. + +### Environment + - OS: [e.g. Arch Linux] + - Other details that you think may affect. + +### Additional context +Add any other context about the problem here. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature-request--.md b/.github/ISSUE_TEMPLATE/feature-request--.md new file mode 100644 index 00000000..3bd987c1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request--.md @@ -0,0 +1,15 @@ +--- +name: "Feature request \U0001F680" +about: Suggest an idea +labels: enhancement + +--- + +## Summary +Brief explanation of the feature. + +### Basic example +Include a basic example or links here. + +### Motivation +Why are we doing this? What use cases does it support? What is the expected outcome? \ No newline at end of file diff --git a/.github/workflows/build_latest.yaml b/.github/workflows/build_latest.yaml index 8cf4888d..13cd81fa 100644 --- a/.github/workflows/build_latest.yaml +++ b/.github/workflows/build_latest.yaml @@ -1,100 +1,48 @@ -name: ESPHome-latest +name: Build-Satellite-Firmware on: - - push - - pull_request - - workflow_dispatch + push: + branches: + - develop + workflow_dispatch: env: DEFAULT_PYTHON: "3.9" - jobs: - common: - name: Create common environment - runs-on: ubuntu-latest - outputs: - cache-key: ${{ steps.cache-key.outputs.key }} - steps: - - name: Check out code from GitHub - uses: actions/checkout@v4.1.6 - - name: Generate cache-key - id: cache-key - run: echo key="${{ hashFiles('requirements.txt') }}" >> $GITHUB_OUTPUT - - name: Set up Python ${{ env.DEFAULT_PYTHON }} - id: python - uses: actions/setup-python@v5.1.0 - with: - python-version: ${{ env.DEFAULT_PYTHON }} - - name: Restore Python virtual environment - id: cache-venv - uses: actions/cache@v4.0.2 - with: - path: venv - # yamllint disable-line rule:line-length - key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ steps.cache-key.outputs.key }} - - name: Create Python virtual environment - if: steps.cache-venv.outputs.cache-hit != 'true' - run: | - python -m venv venv - . venv/bin/activate - python --version - pip install -r requirements.txt - build-list: - runs-on: ubuntu-latest - outputs: - matrix: ${{ steps.set-matrix.outputs.matrix }} - steps: - - name: Check out code from GitHub - uses: actions/checkout@v4.1.6 - - name: Find all YAML satellite files - id: set-matrix - run: echo "matrix=$(ls config/satellite*.yaml | jq -R -s -c 'split("\n")[:-1]')" >> $GITHUB_OUTPUT + build-firmware: + uses: esphome/workflows/.github/workflows/build.yml@main + with: + files: | + config/satellite1.yaml + esphome-version: 2024.11.2 + release-summary: develop-branch + release-url: + release-version: - build: - name: Build YAML config ${{ matrix.file }} - runs-on: ubuntu-latest + create_artifact_matrix: + name: Create artifact matrix needs: - - common - - build-list - strategy: - fail-fast: false - max-parallel: 2 - matrix: - file: ${{ fromJson(needs.build-list.outputs.matrix) }} + - build-firmware + outputs: + matrix: ${{ steps.artifacts.outputs.matrix }} + runs-on: ubuntu-latest steps: - - name: Check out code from GitHub - uses: actions/checkout@v4.1.6 - - name: Restore Python - uses: ./.github/actions/restore-python - with: - python-version: ${{ env.DEFAULT_PYTHON }} - cache-key: ${{ needs.common.outputs.cache-key }} - - - name: Run esphome compile ${{ matrix.file }} - run: | - . venv/bin/activate - esphome compile ${{ matrix.file }} - esphome config ${{ matrix.file }} >> ${{ matrix.file }}.cmpl.yaml - - - name: Parse name - id: get_node_name - uses: mikefarah/yq@master + - name: Download artifacts + uses: actions/download-artifact@v4.1.8 with: - cmd: yq '.esphome.name' ${{ matrix.file }}.cmpl.yaml + path: files - - name: 'Upload Artifact' - uses: actions/upload-artifact@v4 - with: - name: ${{ steps.get_node_name.outputs.result }}.firmware.factory.bin - path: config/.esphome/build/${{ steps.get_node_name.outputs.result }}/.pioenvs/${{ steps.get_node_name.outputs.result }}/firmware.factory.bin - retention-days: 30 + - run: tree - - name: 'Upload Artifact' - uses: actions/upload-artifact@v4 - with: - name: ${{ steps.get_node_name.outputs.result }}.firmware.bin - path: config/.esphome/build/${{ steps.get_node_name.outputs.result }}/.pioenvs/${{ steps.get_node_name.outputs.result }}/firmware.bin - retention-days: 30 \ No newline at end of file + - name: Get artifact names + id: artifacts + run: | + artifacts=$(ls --format=single-column files) + echo "artifacts<> $GITHUB_OUTPUT + echo "$artifacts" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + echo "matrix=$(ls files | jq -R -s -c 'split("\n")[:-1]')" >> $GITHUB_OUTPUT + \ No newline at end of file diff --git a/.github/workflows/build_release.yaml b/.github/workflows/build_release.yaml new file mode 100644 index 00000000..6db096b2 --- /dev/null +++ b/.github/workflows/build_release.yaml @@ -0,0 +1,166 @@ +name: Build-and-Release-Satellite-Firmware + +on: + push: + branches: + - main + - staging + workflow_dispatch: + inputs: + tag: + description: 'Tag for the release (optional). If not provided, it will be auto-generated.' + required: false + +env: + DEFAULT_PYTHON: "3.9" +permissions: + contents: write + pull-requests: read +jobs: + prepare: + runs-on: ubuntu-latest + outputs: + commit_id: ${{ steps.dump.outputs.commit }} + previous_tag: ${{ steps.dump.outputs.previous_tag }} + next_tag: ${{ steps.set_release_tag.outputs.release_tag }} + steps: + - name: Check out code from GitHub + uses: actions/checkout@v4.1.6 + with: + fetch-depth: 0 + + - name: Set Manual Tag (if provided) + id: set_tag + if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag != '' }} + run: echo "manual_tag=${{ github.event.inputs.tag }}" >> $GITHUB_OUTPUT + + - name: Bump version + # if manual tag is provided, use it + if: ${{ !steps.set_tag.outputs.manual_tag }} + uses: anothrNick/github-tag-action@1.70.0 + id: bump + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # To create a major bump add #major to commit message + DEFAULT_BUMP: 'patch' + WITH_V: true + PRERELEASE: ${{ github.ref_name != 'main' }} + PRERELEASE_SUFFIX: ${{ github.ref_name == 'staging' && 'beta' || 'alpha' }} + DRY_RUN: true # Prevent action from pushing the tag. + INITIAL_VERSION: 0.0.0 + + - name: Set tag for release + if: ${{ steps.set_tag.outputs.manual_tag || steps.bump.outputs.tag }} + id: set_release_tag + run: | + if [ -z "${{ steps.set_tag.outputs.manual_tag }}" ]; then + echo "release_tag=${{ steps.bump.outputs.tag }}" >> $GITHUB_OUTPUT + else + echo "release_tag=${{ steps.set_tag.outputs.manual_tag }}" >> $GITHUB_OUTPUT + fi + + - id: dump + name: Set outputs + run: | + echo "gh_env=${{ github.ref_name == 'main' && 'production' || github.ref_name == 'staging' && 'beta' || 'alpha' }}" >> $GITHUB_OUTPUT + echo "commit=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT + echo "long_version=${{ steps.set_release_tag.outputs.release_tag }}_$(date +%Y-%m-%d)_$(git rev-parse HEAD)" >> $GITHUB_OUTPUT + echo "label_version=$(echo "${{ steps.set_release_tag.outputs.release_tag }}" | sed 's/\./dot/g' | sed 's/-/dash/g')" >> $GITHUB_OUTPUT + echo "previous_tag=$(git describe --abbrev=0 --tags)" >> $GITHUB_OUTPUT + echo "files=$(ls config/satellite*.yaml | jq --slurp --raw-input )" >> $GITHUB_OUTPUT + + - name: Update YAML files + run: | + files=$(echo ${{ steps.dump.outputs.files }} \ + | sed 's/^"//' \ + | sed 's/"$//' \ + | sed 's/\\n/\n/g') + tag="${{ steps.set_release_tag.outputs.release_tag }}" + for file in $files; do + echo "Updating $file" + # Use sed to match the esp32_fw_version line and replace its value + sed -i "s/^\(\s*esp32_fw_version:\s*\).*/\1\"${tag}\"/" "$file" + done + build-firmware: + name: Build Firmware + needs: + - prepare + uses: esphome/workflows/.github/workflows/build.yml@main + with: + files: | + config/satellite1.yaml + esphome-version: 2024.11.2 + release-version: ${{ needs.prepare.outputs.next_tag }} + + push-tag: + name: Push tag to repository and build changelog. + needs: + - prepare + - build-firmware + runs-on: ubuntu-latest + outputs: + changelog: ${{ steps.changelog.outputs.changelog }} + + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Bump version and push tag + if: ${{ !github.event.inputs.tag }} + uses: anothrNick/github-tag-action@1.70.0 + id: bump + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # To create a major bump add #major to commit message + DEFAULT_BUMP: 'patch' + WITH_V: true + PRERELEASE: ${{ github.ref_name != 'main' }} + PRERELEASE_SUFFIX: ${{ github.ref_name == 'staging' && 'beta' || 'alpha' }} + DRY_RUN: false # Set the tag to the commit. + INITIAL_VERSION: 0.0.0 + + - name: Release Changelog Builder + id: changelog + uses: mikepenz/release-changelog-builder-action@v5 + with: + fromTag: ${{ needs.prepare.outputs.previous_tag }} + + create_release: + name: Create Release + # only build release for staging and main branches + # if: github.ref_name == 'main' || github.ref_name == 'staging' + needs: + - push-tag + - prepare + runs-on: ubuntu-latest + steps: + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + path: files + + - name: Create zip files + run: | + mkdir -p build + echo "Copying all files to build directory" + find files -name "*.bin" -exec cp {} build/ \; + + - name: Create release + uses: softprops/action-gh-release@v2 + with: + files: build/*.bin + generate_release_notes: true + append_body: true + body: ${{ needs.push-tag.outputs.changelog }} + tag_name: ${{ needs.prepare.outputs.next_tag }} + + - name: Update Documentation + run: | + curl -X POST \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${{ secrets.SECOND_REPO_PAT }}" \ + https://api.github.com/repos/FutureProofHomes/Documentation/actions/workflows/update-binaries-esp32.yaml/dispatches \ + -d '{"ref":"main", "inputs": {"esphome_release_tag": "${{ needs.prepare.outputs.next_tag }}"}}' + env: + SECOND_REPO_PAT: ${{ secrets.SECOND_REPO_PAT }} diff --git a/.gitignore b/.gitignore index caf0b675..66ec044f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ __pycache__ *.pyc .venv/ -**/.esphome/ *.egg-info/ .esphome_repo @@ -17,4 +16,11 @@ managed_components/ sdkconfig.esp32-idf sdkconfig.esp32-s3-idf -*/**/secrets.yaml +**/.esphome/ +**/secrets.yaml + +/testdata +/wake-word-benchmark/ +/Docker/firmware_server/shared-folder + +**/.DS_Store \ No newline at end of file diff --git a/Docker/firmware_server/Dockerfile b/Docker/firmware_server/Dockerfile new file mode 100644 index 00000000..52d831a6 --- /dev/null +++ b/Docker/firmware_server/Dockerfile @@ -0,0 +1 @@ +FROM nginx:alpine \ No newline at end of file diff --git a/Docker/firmware_server/config/default.conf b/Docker/firmware_server/config/default.conf new file mode 100644 index 00000000..fddb0708 --- /dev/null +++ b/Docker/firmware_server/config/default.conf @@ -0,0 +1,15 @@ +server { + listen 80; + server_name localhost; + + location / { + root /usr/share/nginx/html; + index index.html index.htm; + autoindex on; + } + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } +} \ No newline at end of file diff --git a/Docker/firmware_server/docker-compose.yaml b/Docker/firmware_server/docker-compose.yaml new file mode 100644 index 00000000..39afaa88 --- /dev/null +++ b/Docker/firmware_server/docker-compose.yaml @@ -0,0 +1,10 @@ +version: '2' + +services: + web: + build: . + volumes: + - ./config/default.conf:/etc/nginx/conf.d/default.conf + - ./shared-folder:/usr/share/nginx/html + ports: + - 8080:80 \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..746e0ab7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,709 @@ +# ESPHome License + +Copyright (c) 2019 ESPHome + +The ESPHome License is made up of two base licenses: MIT and the GNU GENERAL PUBLIC LICENSE. +The C++/runtime codebase of the ESPHome project (file extensions .c, .cpp, .h, .hpp, .tcc, .ino) are +published under the GPLv3 license. The python codebase and all other parts of this codebase are +published under the MIT license. + +Both MIT and GPLv3 licenses are attached to this document. + +## MIT License + +Copyright (c) 2019 ESPHome + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +## GPLv3 License + + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, 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 +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If 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 convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU 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 +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "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 PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM 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 PROGRAM (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 PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state 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 program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. \ No newline at end of file diff --git a/README.md b/README.md index 4f2ebd36..be4f67de 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,262 @@ -## Flashing the ESP32 + + + + + + +[![Contributors][contributors-shield]][contributors-url] +[![Forks][forks-shield]][forks-url] +[![Stargazers][stars-shield]][stars-url] +[![Issues][issues-shield]][issues-url] +[![MIT License][license-shield]][license-url] +[![LinkedIn][linkedin-shield]][linkedin-url] + + + +
+
+ + Logo + + +

Satellite1 Core Board ESPHome Firmware

+ +

+ Open-Source ESPHome Firmware for Your Private AI-Powered Satellite1 Voice Assistant & Multisensor +
+ Explore the docs » +
+
+ View Demos + · + Report Bug + · + Request Feature +

+
+ + + + +
+ Table of Contents +
    +
  1. + About The Project + +
  2. +
  3. + Getting Started + +
  4. +
  5. Usage
  6. +
  7. Roadmap
  8. +
  9. Contributing
  10. +
  11. License
  12. +
  13. Contact
  14. +
  15. Acknowledgments
  16. +
+
+ + + + +## About the Project +The Satellite1 ESPHome firmware should be flashed on your [FutureProofHomes Core Board](https://futureproofhomes.net/products/satellite1-core-board). For flashing instructions please visit [Docs.FutureProofHomes.net](https://docs.futureproofhomes.net). After the firmware is successfully flashed and your Core Board is connected to your Wifi it will appear in your Home Assistant as a new device called "Satellite1". + +## Key Features of the Firmware +- [ ] Works with the Home Assistant Platform so you can control your home. +- [ ] Optionally connect the Satellite1 to [Local AI Ollama](https://www.home-assistant.io/integrations/ollama/) or [OpenAI ChatGPT](https://www.home-assistant.io/integrations/openai_conversation/) to chat with an AI and control your home. Do this at your own risk. +- [ ] This firmware enables your FutureProofHomes Core Board to mount with our [HAT board](https://futureproofhomes.net/products/satellite1-top-microphone-board) which then unlocks: +- [ ] On-Demand flashing of our open source XMOS firmware for audio echo cancellation and other audio processing algorithms. +- [ ] [On-Device WakeWord support.](https://github.com/kahrendt/microWakeWord) +- [ ] Temperature/Humidity/Light sensor readings of the room +- [ ] Attachable mmWave Radar for Human Presence Detection +- [ ] Music streaming via HA Media Browser or [Music Assistant](https://music-assistant.io/) +- [ ] Volume Up/Down & Action Buttons +- [ ] Hardware & Software Mute Buttons +- [ ] 360 degree LEDs & Notification Animations +- [ ] Support for TTS Announcements via Home Assistant +- [ ] USB-C Power Delivery for easy power input +- [ ] GPIO expansion ports for to quickly add accessories like speakers, sensors, radios, amplifiers, etc. +- [ ] (COMING SOON) Bluetooth Room Presense Detection (currently this feature is crashing) + +## Why Open Source? +We believe it is irresponsible to ask customers to trust that our microphone and AI in-a-box protects your privacy. To hold ourselves and the whole world accountable it is prudent to open-source our work so that we can all benefit from this amazing technology. Let's build together. + +## Why Purchase from FutureProofHomes? +Put simply, your purchase helps fund our team and further innovation. Also, the FutureProofHomes team will work hard to give you top-quality products that are tested, fully-functional, in stock (as often as possible) and lead with great community support. You can purchase Satellite1 components individually, or purchase the entire devkit as a package. Help us, help you! + +

(back to top)

+ + +### Built With + +* [![ESPHome][esphome.io]][esphome-url] + +

(back to top)

+ + + + +## Getting Started + +Go to [Docs.FutureProofHomes.net](https://docs.futureproofhomes.net) and follow the instructions to assemble, flash and set up your Core Board. + + + +### Prerequisites + +- FutureProofHomes Core Board & USB-C cable to plug into your computer. +- Highly recommend our FutureProofHomes HAT board to unlock all the features. + + + + + +## Usage + + + +_For more examples, please refer to the [Documentation](https://docs.futureproofhomes.net)_ + +

(back to top)

+ + + + +## Core Board Roadmap + +- [ ] TBD + +See the [open issues](https://github.com/FutureProofHomes/Satellite1-ESPHome/issues) for a full list of proposed features (and known issues). + +

(back to top)

+ + + + +## Contributing + +Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. + +If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". +Don't forget to give the project a star! Thanks again! + +1. Fork the Project +3. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) +4. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) +5. Push to the Branch (`git push origin feature/AmazingFeature`) +6. Open a Pull Request + +

(back to top)

+ + +## Developing, Testing & Debugging Create/activate environment by running from project root: ```bash source scripts/setup_build_env.sh ``` -build firmware: +Build firmware on your local machine: ```bash -esphome compile config/satellite_mic_test.yaml +esphome compile config/satellite1.yaml ``` -upload firmware: +Upload firmware to your Core Board: ```bash -esphome upload config/satellite_mic_test.yaml +esphome upload config/satellite1.yaml ``` -setup wifi: +Connect the device to your wifi network: +1. Go to: [web.esphome.io](https://web.esphome.io) +2. Click on "Connect" and select the correct JTAG/serial debug unit +3. Click on the three dots and select 'Configure WiFi' -go to: https://web.esphome.io/?dashboard_wizard +Tail the Core Board's Logs: +1. Go to: [web.esphome.io](https://web.esphome.io) and connect then click logs... or +2. Tail the ESPHome logs of the Core Board's running firmware from the command line: +```bash +esphome logs config/satellite1.yaml +``` -click on connet and select the usb port +## Home Assistant Voice Assistant Debugging -click on the three dots and select 'Configure WiFi' +1. [Set up you local pipeline](https://www.home-assistant.io/voice_control/voice_remote_local_assistant/) +2. [Troubleshoot your pipeline](https://www.home-assistant.io/voice_control/troubleshooting/) -check the logs: -```bash -esphome logs config/satellite_mic_test.yaml -``` + +## License +Distributed under the ESPHOME License. See `LICENSE.txt` for more information. +

(back to top)

-## Setting up Home Assistant -Setup voice assistant pipeline: -https://www.home-assistant.io/voice_control/voice_remote_local_assistant/ + +## Contact +FutureProofHomes - [Website](https://futureproofhomes.net/) -enable recording of voice assistant streams: +

(back to top)

-add to your configuration.yaml: -```yaml -assist_pipeline: - debug_recording_dir: /config/debug -``` + +## YouTube + +Checkout out our growing YouTube Channel - [YouTube.com/@FutureProofHomes](https://www.youtube.com/@futureproofhomes) + + +

(back to top)

+ + +## Acknowledgments + +* @gnumpi for all the amazing C code +* @qnlbnsl for all the Github Action & automated release work +* [Nabu Casa](https://nabucasa.com) for making this all possible +* Your name here soon... + +

(back to top)

+ + + + + +[contributors-shield]: https://img.shields.io/github/contributors/FutureProofHomes/Satellite1-ESPHome.svg?style=for-the-badge +[contributors-url]: https://github.com/FutureProofHomes/Satellite1-ESPHome/graphs/contributors +[forks-shield]: https://img.shields.io/github/forks/FutureProofHomes/Satellite1-ESPHome.svg?style=for-the-badge +[forks-url]: https://github.com/FutureProofHomes/Satellite1-ESPHome/network/members +[stars-shield]: https://img.shields.io/github/stars/FutureProofHomes/Satellite1-ESPHome.svg?style=for-the-badge +[stars-url]: https://github.com/FutureProofHomes/Satellite1-ESPHome/stargazers +[issues-shield]: https://img.shields.io/github/issues/FutureProofHomes/Satellite1-ESPHome.svg?style=for-the-badge +[issues-url]: https://github.com/FutureProofHomes/Satellite1-ESPHome/issues +[license-shield]: https://img.shields.io/github/license/FutureProofHomes/Satellite1-ESPHome.svg?style=for-the-badge +[license-url]: https://github.com/FutureProofHomes/Satellite1-ESPHome/blob/master/LICENSE.txt +[linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=for-the-badge&logo=linkedin&colorB=555 +[linkedin-url]: https://linkedin.com/in/linkedin_username +[genaimockup]: assets/images/mockup.png +[combo_render]: assets/images/combo_render.png +[kicad.org]: https://img.shields.io/badge/KiCad-314CB0?style=for-the-badge&logo=kicad&logoColor=white +[kicad-url]: https://www.kicad.org/ +[esphome.io]: https://img.shields.io/badge/-ESPHome-000000?style=for-the-badge&logo=esphome&logoColor=white +[esphome-url]: https://esphome.io/ \ No newline at end of file diff --git a/assets/images/logo.png b/assets/images/logo.png new file mode 100644 index 00000000..7830469f Binary files /dev/null and b/assets/images/logo.png differ diff --git a/config/common/.gitignore b/config/common/.gitignore new file mode 100644 index 00000000..da72d57e --- /dev/null +++ b/config/common/.gitignore @@ -0,0 +1,4 @@ +# Gitignore settings for ESPHome +# This is an example and may include too much for your use-case. +# You can modify this file to suit your needs. +/secrets.yaml diff --git a/config/common/buttons.yaml b/config/common/buttons.yaml new file mode 100644 index 00000000..8d0f8a7d --- /dev/null +++ b/config/common/buttons.yaml @@ -0,0 +1,259 @@ +globals: + # Global variable tracking if the volume button was recently touched. + - id: volume_buttons_touched + type: bool + restore_value: no + initial_value: 'false' + + # Global variable tracking if the action button was recently touched. + - id: action_button_touched + type: bool + restore_value: no + initial_value: 'false' + +event: + # Event entity exposed to the user to automate on complex action button presses. + # The simple press is not exposed as it is used to control the device itself. + - platform: template + id: action_button_press_event + name: "Action Button Press" + icon: mdi:button-pointer + device_class: button + event_types: + - single_press + - double_press + - triple_press + - long_press + +binary_sensor: + - platform: gpio + id: btn_up + pin: + satellite1: + port: INPUT_A + pin: 0 + inverted: true + name: "Button Up (Vol+)" + icon: "mdi:volume-plus" + on_press: + then: + - lambda: id(volume_buttons_touched) = true; + - script.execute: + id: control_volume + increase_volume: true + + - platform: gpio + id: btn_down + pin: + satellite1: + port: INPUT_A + pin: 2 + inverted: true + name: "Button Down (Vol-)" + icon: "mdi:volume-minus" + on_press: + then: + - lambda: id(volume_buttons_touched) = true; + - script.execute: + id: control_volume + increase_volume: false + + - platform: gpio + id: btn_left + pin: + satellite1: + port: INPUT_A + pin: 3 + inverted: false + name: "Button Left (HW Mute)" + icon: "mdi:microphone-off" + on_state: + - if: + condition: + binary_sensor.is_off: btn_left + then: + - script.execute: + id: play_sound + priority: false + sound_file: !lambda return id(mute_switch_off_sound); + - switch.template.publish: + id: master_mute_switch + state: OFF + else: + - script.execute: + id: play_sound + priority: false + sound_file: !lambda return id(mute_switch_on_sound); + - switch.template.publish: + id: master_mute_switch + state: ON + + - platform: gpio + id: btn_action + pin: + number: 0 + inverted: true + mode: + input: true + pullup: true + ignore_strapping_warning: true + name: "Button Right (Action)" + icon: "mdi:gesture-tap" + on_press: + - script.execute: control_leds + on_release: + - script.execute: control_leds + on_multi_click: + # Simple Click: + # - Abort "things" in order + # - Timer + # - Announcements + # - Voice Assistant Pipeline run + # - Music + # - Starts the voice assistant if it is not yet running and if the device is not muted. + - timing: + - ON for at most 1s + - OFF for at least 0.25s + then: + - if: + condition: + lambda: return !id(init_in_progress); + then: + - event.trigger: + id: action_button_press_event + event_type: "single_press" + - if: + condition: + switch.is_on: timer_ringing + then: + - switch.turn_off: timer_ringing + else: + - if: + condition: + lambda: return id(nabu_media_player)->state == media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING; + then: + - lambda: | + id(nabu_media_player) + ->make_call() + .set_command(media_player::MediaPlayerCommand::MEDIA_PLAYER_COMMAND_STOP) + .set_announcement(true) + .perform(); + else: + - if: + condition: + voice_assistant.is_running: + then: + - voice_assistant.stop: + else: + - if: + condition: + media_player.is_playing: + then: + - media_player.pause: + else: + - if: + condition: + and: + - switch.is_off: master_mute_switch + - not: + voice_assistant.is_running + then: + - script.execute: + id: play_sound + priority: true + sound_file: !lambda return id(center_button_press_sound); + - delay: 300ms + - voice_assistant.start: + # Double Click + # . Exposed as an event entity. To be used in automations inside Home Assistant + - timing: + - ON for at most 1s + - OFF for at most 0.25s + - ON for at most 1s + - OFF for at least 0.25s + then: + - if: + condition: + lambda: return !id(init_in_progress); + then: + - script.execute: + id: play_sound + priority: false + sound_file: !lambda return id(center_button_double_press_sound); + - event.trigger: + id: action_button_press_event + event_type: "double_press" + # Triple Click + # . Exposed as an event entity. To be used in automations inside Home Assistant + - timing: + - ON for at most 1s + - OFF for at most 0.25s + - ON for at most 1s + - OFF for at most 0.25s + - ON for at most 1s + - OFF for at least 0.25s + then: + - if: + condition: + lambda: return !id(init_in_progress); + then: + - script.execute: + id: play_sound + priority: false + sound_file: !lambda return id(center_button_triple_press_sound); + - event.trigger: + id: action_button_press_event + event_type: "triple_press" + # Long Press + # . Exposed as an event entity. To be used in automations inside Home Assistant + - timing: + - ON for at least 1s + then: + - if: + condition: + lambda: return !id(init_in_progress); + then: + - script.execute: + id: play_sound + priority: false + sound_file: !lambda return id(center_button_long_press_sound); + - light.turn_off: voice_assistant_leds + - event.trigger: + id: action_button_press_event + event_type: "long_press" + + # Factory Reset Warning + # . Audible and Visible warning. + - timing: + - ON for at least 10s + then: + - if: + condition: + lambda: return !id(action_button_touched); + then: + - light.turn_on: + brightness: 100% + id: voice_assistant_leds + effect: "Factory Reset Coming Up" + - script.execute: + id: play_sound + priority: true + sound_file: !lambda return id(factory_reset_initiated_sound); + - wait_until: + binary_sensor.is_off: btn_action + - light.turn_off: voice_assistant_leds + - script.execute: + id: play_sound + priority: true + sound_file: !lambda return id(factory_reset_cancelled_sound); + + # Factory Reset + - timing: + - ON for at least 22s + then: + - if: + condition: + lambda: return !id(action_button_touched); + then: + - button.press: factory_reset_button + diff --git a/config/common/core_board.yaml b/config/common/core_board.yaml new file mode 100644 index 00000000..45d0d0e2 --- /dev/null +++ b/config/common/core_board.yaml @@ -0,0 +1,42 @@ +esphome: + platformio_options: + board_build.flash_mode: dio + board_upload.maximum_size: 16777216 + +esp32: + board: esp32-s3-devkitc-1 + variant: ESP32S3 + flash_size: 16MB + framework: + type: esp-idf + version: recommended + sdkconfig_options: + CONFIG_ESP32_S3_BOX_BOARD: "y" + CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP: "y" + CONFIG_BT_ALLOCATION_FROM_SPIRAM_FIRST: "y" + CONFIG_BT_BLE_DYNAMIC_ENV_MEMORY: "y" + +psram: + mode: octal + speed: 80MHz + +i2c: + - id: i2c_0 + sda: GPIO5 + scl: GPIO6 + frequency: 400kHz + scan: True + +spi: + - id: spi_0 + clk_pin: GPIO12 + mosi_pin: GPIO11 + miso_pin: GPIO13 + interface: SPI2 + +i2s_audio: + - id: i2s_shared + i2s_lrclk_pin: GPIO07 + i2s_bclk_pin: GPIO08 + i2s_mclk_pin: GPIO16 + access_mode: duplex diff --git a/config/common/debug.yaml b/config/common/debug.yaml new file mode 100644 index 00000000..16e41541 --- /dev/null +++ b/config/common/debug.yaml @@ -0,0 +1,64 @@ + +external_components: + - source: + type: local + path: ../esphome/components + components: [ version ] + +debug: + update_interval: 5s + +text_sensor: + - platform: debug + device: + name: "Device Info" + entity_category: "diagnostic" + reset_reason: + name: "Reset Reason" + entity_category: "diagnostic" + + - platform: version + name: Satellite1 Firmware Build + entity_category: "diagnostic" + hide_timestamp: true + + - platform: template + name: "Current XMOS fw" + entity_category: "diagnostic" + icon: "mdi:code-braces" + lambda: |- + return std::string("${xmos_fw_version}"); + + - platform: template + name: "Compat. w/ CORE hw" + entity_category: "diagnostic" + icon: "mdi:chip" + lambda: |- + return std::string("${built_for_core_hw_version}"); + + - platform: template + name: "Compat. w/ HAT hw" + entity_category: "diagnostic" + icon: "mdi:chip" + lambda: |- + return std::string("${built_for_hat_hw_version}"); + +sensor: + - platform: debug + free: + name: "Heap Free" + entity_category: "diagnostic" + block: + name: "Heap Max Block" + entity_category: "diagnostic" + loop_time: + name: "Loop Time" + entity_category: "diagnostic" + psram: + name: "Free PSRAM" + entity_category: "diagnostic" + - platform: wifi_signal + name: "Wi-Fi Signal Strength" + entity_category: "diagnostic" + update_interval: 60s + diff --git a/config/common/hat_sensors.yaml b/config/common/hat_sensors.yaml new file mode 100644 index 00000000..e6556255 --- /dev/null +++ b/config/common/hat_sensors.yaml @@ -0,0 +1,63 @@ +sensor: + - platform: aht10 + id: "aht20_temp_hum_sensor" + address: 0x38 + variant: AHT20 + temperature: + name: Temperature + id: temperature_sensor + filters: + - offset: ${temp_offset} + - lambda: "return x + id(temp_offset_ui).state;" + humidity: + name: Humidity + id: humidity_sensor + filters: + - offset: ${humidity_offset} + - lambda: "return x + id(humidity_offset_ui).state;" + + + - platform: ltr_als_ps + address: 0x29 + type: ALS + update_interval: 60s + + # short variant of sensor definition: + ambient_light: "Ambient light" + + +number: + #Dynamically adjust the temperature and humidity offset. Credit goes to Lewis over @ EverythingSmartHome. I admire your work and hope to meet someday. Beers on me. + - platform: template + name: "Offset Temperature" + id: temp_offset_ui + unit_of_measurement: "°C" + min_value: -20 + max_value: 20 + step: 0.1 + mode: box + update_interval: never + optimistic: true + restore_value: true + initial_value: 0 + icon: "mdi:thermometer" + entity_category: config + on_value: + - lambda: 'id(aht20_temp_hum_sensor).update();' + + - platform: template + name: "Offset Humidity" + id: humidity_offset_ui + unit_of_measurement: "%" + min_value: -50 + max_value: 50 + step: 0.1 + mode: box + update_interval: never + optimistic: true + restore_value: true + initial_value: 0 + icon: "mdi:water-percent" + entity_category: config + on_value: + - lambda: 'id(aht20_temp_hum_sensor).update();' \ No newline at end of file diff --git a/config/common/home_assistant.yaml b/config/common/home_assistant.yaml new file mode 100644 index 00000000..c25c6fd6 --- /dev/null +++ b/config/common/home_assistant.yaml @@ -0,0 +1,100 @@ +api: + id: api_id + on_client_connected: + - lambda: id(init_in_progress) = false; + - script.execute: control_leds + on_client_disconnected: + - script.execute: control_leds + + +button: + # Restarts Sat1 Device + - platform: restart + name: "Restart Sat1" + entity_category: diagnostic + + # Restarts Sat1 to safe mode + - platform: safe_mode + name: "Restart Sat1 (Safe Mode)" + entity_category: diagnostic + + # Restores the Sat1 ESP back to factory settings + #TODO: What buttons do we want to long press to initiate this + #TODO: Create the factory reset firmware + - platform: factory_reset + id: factory_reset_button + name: "Factory Reset ESP32 FW" + entity_category: diagnostic + internal: true + + # Flashes Satellite1 with most recent XMOS firmware + #TODO: This needs to grab the latest firmware and not hardcoded version + #TODO: The LED ring does not show a visual status when flashing because we connect to the LEDs via XMOS and can't connect during flashing. Hrmmmmmm.... + - platform: template + id: flash_satellite + name: "Flash XMOS ${xmos_fw_version}" + entity_category: diagnostic + on_press: + then: + - lambda: id(xmos_flashing_started) = true; + - script.execute: control_leds + - ota.satellite1.flash: + url: https://raw.githubusercontent.com/FutureProofHomes/Documentation/refs/heads/main/assets/firmware/xmos/${xmos_fw_version}/satellite1_firmware_fixed_delay.factory.bin + md5_url: https://raw.githubusercontent.com/FutureProofHomes/Documentation/refs/heads/main/assets/firmware/xmos/${xmos_fw_version}/satellite1_firmware_fixed_delay.factory.md5 + + +switch: + # Wake Word Sound Switch. + - platform: template + id: wake_sound + name: Wake sound + icon: "mdi:bullhorn" + entity_category: config + optimistic: true + restore_mode: RESTORE_DEFAULT_ON + + # This is the master mute switch. It is exposed to Home Assistant. The user can only turn it on and off if the hardware switch is off. (The hardware switch overrides the software one) + - platform: template + id: master_mute_switch + restore_mode: RESTORE_DEFAULT_OFF + icon: "mdi:microphone-off" + name: Mute Microphones + entity_category: config + turn_on_action: + - if: + condition: + binary_sensor.is_off: btn_left + then: + - switch.template.publish: + id: master_mute_switch + state: ON + turn_off_action: + - if: + condition: + binary_sensor.is_off: btn_left + then: + - switch.template.publish: + id: master_mute_switch + state: OFF + else: + - lambda: id(warning) = true; + - script.execute: control_leds + on_turn_on: + - script.execute: control_leds + on_turn_off: + - script.execute: control_leds + +text_sensor: + - platform: template + id: pd_state_text + name: "USB-C Power Draw" + icon: "mdi:usb-c-port" + entity_category: "diagnostic" + update_interval: never + lambda: |- + if( id(pd_fusb302b).state == power_delivery::PD_STATE_DISCONNECTED){ + return std::string("Detached"); + } else { + return id(pd_fusb302b).contract; + } + diff --git a/config/common/led_ring.yaml b/config/common/led_ring.yaml new file mode 100644 index 00000000..c7a61edc --- /dev/null +++ b/config/common/led_ring.yaml @@ -0,0 +1,703 @@ + +globals: + # Global index for our LEDs. So that switching between different animation does not lead to unwanted effects. + - id: global_led_animation_index + type: int + restore_value: no + initial_value: '0' + +light: + # Hardware LED ring. Not used because remapping needed + - platform: esp32_rmt_led_strip + id: hw_led_ring + pin: GPIO21 + rmt_channel: 1 + num_leds: 24 + rgb_order: GRB + chipset: WS2812 + + # User facing LED ring. Remapping of the hardware LEDs. + # Exposed to be used by the user. + - platform: partition + id: led_ring + name: LED Ring + entity_category: config + icon: "mdi:dots-circle" + default_transition_length: 0ms + restore_mode: RESTORE_DEFAULT_OFF + initial_state: + color_mode: rgb + brightness: 66% + red: 9.4% + green: 73.3% + blue: 94.9% + segments: + - id: hw_led_ring + from: 0 + to: 23 + + # Voice Assistant LED ring. Remapping of the hardware LED. + # This light is not exposed. The device controls it + - platform: partition + id: voice_assistant_leds + internal: true + default_transition_length: 0ms + segments: + - id: hw_led_ring + from: 0 + to: 23 + effects: + - addressable_lambda: + name: "Waiting for Command" + update_interval: 100ms + lambda: |- + auto light_color = id(led_ring).current_values; + Color color(light_color.get_red() * 255, light_color.get_green() * 255, + light_color.get_blue() * 255); + for (int i = 0; i < 24; i++) { + if (i == id(global_led_animation_index) % 24) { + it[i] = color; + } else if (i == (id(global_led_animation_index) + 23) % 24) { + it[i] = color * 192; + } else if (i == (id(global_led_animation_index) + 22) % 24) { + it[i] = color * 128; + } else if (i == (id(global_led_animation_index) + 12) % 24) { + it[i] = color; + } else if (i == (id(global_led_animation_index) + 11) % 24) { + it[i] = color * 192; + } else if (i == (id(global_led_animation_index) + 10) % 24) { + it[i] = color * 128; + } else { + it[i] = Color::BLACK; + } + } + id(global_led_animation_index) = (id(global_led_animation_index) + 1) % 24; + - addressable_lambda: + name: "Listening For Command" + update_interval: 50ms + lambda: |- + auto light_color = id(led_ring).current_values; + Color color(light_color.get_red() * 255, light_color.get_green() * 255, + light_color.get_blue() * 255); + for (int i = 0; i < 24; i++) { + if (i == id(global_led_animation_index) % 24) { + it[i] = color; + } else if (i == (id(global_led_animation_index) + 23) % 24) { + it[i] = color * 192; + } else if (i == (id(global_led_animation_index) + 22) % 24) { + it[i] = color * 128; + } else if (i == (id(global_led_animation_index) + 12) % 24) { + it[i] = color; + } else if (i == (id(global_led_animation_index) + 11) % 24) { + it[i] = color * 192; + } else if (i == (id(global_led_animation_index) + 10) % 24) { + it[i] = color * 128; + } else { + it[i] = Color::BLACK; + } + } + id(global_led_animation_index) = (id(global_led_animation_index) + 1) % 24; + - addressable_lambda: + name: "Thinking" + update_interval: 10ms + lambda: |- + static uint8_t brightness_step = 0; + static bool brightness_decreasing = true; + static uint8_t brightness_step_number = 10; + if (initial_run) { + brightness_step = 0; + brightness_decreasing = true; + } + auto light_color = id(led_ring).current_values; + Color color(light_color.get_red() * 255, light_color.get_green() * 255, + light_color.get_blue() * 255); + for (int i = 0; i < 24; i++) { + if (i == id(global_led_animation_index) % 24) { + it[i] = color * uint8_t(255/brightness_step_number*(brightness_step_number-brightness_step)); + } else if (i == (id(global_led_animation_index) + 12) % 24) { + it[i] = color * uint8_t(255/brightness_step_number*(brightness_step_number-brightness_step)); + } else { + it[i] = Color::BLACK; + } + } + if (brightness_decreasing) { + brightness_step++; + } else { + brightness_step--; + } + if (brightness_step == 0 || brightness_step == brightness_step_number) { + brightness_decreasing = !brightness_decreasing; + } + - addressable_lambda: + name: "Replying" + update_interval: 50ms + lambda: |- + id(global_led_animation_index) = (24 + id(global_led_animation_index) - 1) % 24; + auto light_color = id(led_ring).current_values; + Color color(light_color.get_red() * 255, light_color.get_green() * 255, + light_color.get_blue() * 255); + for (int i = 0; i < 24; i++) { + if (i == (id(global_led_animation_index)) % 24) { + it[i] = color; + } else if (i == ( id(global_led_animation_index) + 1) % 24) { + it[i] = color * 192; + } else if (i == ( id(global_led_animation_index) + 2) % 24) { + it[i] = color * 128; + } else if (i == ( id(global_led_animation_index) + 12) % 24) { + it[i] = color; + } else if (i == ( id(global_led_animation_index) + 13) % 24) { + it[i] = color * 192; + } else if (i == ( id(global_led_animation_index) + 14) % 24) { + it[i] = color * 128; + } else { + it[i] = Color::BLACK; + } + } + - addressable_lambda: + name: "Muted or Silent" + update_interval: 16ms + lambda: |- + static int8_t index = 0; + Color muted_color(255, 0, 0); + auto light_color = id(led_ring).current_values; + Color color(light_color.get_red() * 255, light_color.get_green() * 255, + light_color.get_blue() * 255); + for (int i = 0; i < 24; i++) { + if ( light_color.get_state() ) { + it[i] = color; + } else { + it[i] = Color::BLACK; + } + } + if ( id(master_mute_switch).state ) { + it[23] = Color::BLACK; + it[0] = muted_color; + it[1] = Color::BLACK; + it[5] = Color::BLACK; + it[6] = muted_color; + it[7] = Color::BLACK; + it[11] = Color::BLACK; + it[12] = muted_color; + it[13] = Color::BLACK; + it[17] = Color::BLACK; + it[18] = muted_color; + it[19] = Color::BLACK; + } + if ( id(nabu_media_player).volume == 0.0f || id(nabu_media_player).is_muted() ) { + it[1] = Color::BLACK; + it[2] = muted_color; + it[3] = muted_color; + it[4] = muted_color; + it[5] = Color::BLACK; + it[7] = Color::BLACK; + it[8] = muted_color; + it[9] = muted_color; + it[10] = muted_color; + it[11] = Color::BLACK; + it[13] = Color::BLACK; + it[14] = muted_color; + it[15] = muted_color; + it[16] = muted_color; + it[17] = Color::BLACK; + it[19] = Color::BLACK; + it[20] = muted_color; + it[21] = muted_color; + it[22] = muted_color; + it[23] = Color::BLACK; + } + - addressable_lambda: + name: "Volume Display" + update_interval: 50ms + lambda: |- + auto light_color = id(led_ring).current_values; + Color color(light_color.get_red() * 255, light_color.get_green() * 255, + light_color.get_blue() * 255); + Color silenced_color(255, 0, 0); + auto volume_ratio = 24.0f * id(nabu_media_player).volume; + for (int i = 0; i < 24; i++) { + if (i <= volume_ratio) { + it[(0+i)%24] = color * min( 255.0f * (volume_ratio - i) , 255.0f ) ; + } else { + it[(0+i)%24] = Color::BLACK; + } + } + if (id(nabu_media_player).volume == 0.0f) { + it[0] = silenced_color; + } + - addressable_lambda: + name: "Action Button Touched" + update_interval: 16ms + lambda: |- + if (initial_run) { + // set voice_assistant_leds light to colors based on led_ring + auto led_ring_cv = id(led_ring).current_values; + auto va_leds_call = id(voice_assistant_leds).make_call(); + va_leds_call.from_light_color_values(led_ring_cv); + va_leds_call.set_brightness( min ( max( id(led_ring).current_values.get_brightness() , 0.2f ) + 0.1f , 1.0f ) ); + va_leds_call.set_state(true); + va_leds_call.perform(); + } + auto light_color = id(voice_assistant_leds).current_values; + Color color(light_color.get_red() * 255, light_color.get_green() * 255, + light_color.get_blue() * 255); + for (int i = 0; i < 24; i++) { + it[i] = color; + } + - addressable_twinkle: + name: "Twinkle" + twinkle_probability: 50% + - addressable_lambda: + name: "Error" + update_interval: 10ms + lambda: |- + static uint8_t brightness_step = 0; + static bool brightness_decreasing = true; + static uint8_t brightness_step_number = 10; + if (initial_run) { + brightness_step = 0; + brightness_decreasing = true; + } + Color error_color(255, 0, 0); + for (int i = 0; i < 24; i++) { + it[i] = error_color * uint8_t(255/brightness_step_number*(brightness_step_number-brightness_step)); + } + if (brightness_decreasing) { + brightness_step++; + } else { + brightness_step--; + } + if (brightness_step == 0 || brightness_step == brightness_step_number) { + brightness_decreasing = !brightness_decreasing; + } + - addressable_lambda: + name: "Warning" + update_interval: 10ms + lambda: |- + static uint8_t brightness_step = 0; + static bool brightness_decreasing = true; + static uint8_t brightness_step_number = 10; + static uint8_t blink_count = 0; // New counter for blink cycles + if (initial_run) { + brightness_step = 0; + brightness_decreasing = true; + blink_count = 0; + } + Color error_color(255, 0, 0); + if (blink_count < 5) { // Only blink 5 times + for (int i = 0; i < 24; i++) { + it[i] = error_color * uint8_t(255/brightness_step_number*(brightness_step_number-brightness_step)); + } + if (brightness_decreasing) { + brightness_step++; + } else { + brightness_step--; + } + if (brightness_step == 0 || brightness_step == brightness_step_number) { + brightness_decreasing = !brightness_decreasing; + if (brightness_step == 0) { // Completed one blink cycle + blink_count++; + } + } + } else { + // Turn off LEDs after 5 blinks + for (int i = 0; i < 24; i++) { + it[i] = Color(0, 0, 0); + } + } + - pulse: + name: "Fast Pulse" + transition_length: 100ms + update_interval: 100ms + min_brightness: 50% + max_brightness: 100% + + - addressable_lambda: + name: "Timer Ring" + update_interval: 10ms + lambda: |- + static uint8_t brightness_step = 0; + static bool brightness_decreasing = true; + static uint8_t brightness_step_number = 10; + if (initial_run) { + brightness_step = 0; + brightness_decreasing = true; + } + auto light_color = id(led_ring).current_values; + Color color(light_color.get_red() * 255, light_color.get_green() * 255, + light_color.get_blue() * 255); + Color muted_color(255, 0, 0); + for (int i = 0; i < 24; i++) { + it[i] = color * uint8_t(255/brightness_step_number*(brightness_step_number-brightness_step)); + } + if ( id(master_mute_switch).state ) { + it[3] = muted_color; + it[9] = muted_color; + } + if (brightness_decreasing) { + brightness_step++; + } else { + brightness_step--; + } + if (brightness_step == 0 || brightness_step == brightness_step_number) { + brightness_decreasing = !brightness_decreasing; + } + - addressable_lambda: + name: "Timer Tick" + update_interval: 100ms + lambda: |- + auto light_color = id(led_ring).current_values; + Color color(light_color.get_red() * 255, light_color.get_green() * 255, + light_color.get_blue() * 255); + Color muted_color(255, 0, 0); + auto timer_ratio = 24.0f * id(first_active_timer).seconds_left / max(id(first_active_timer).total_seconds , static_cast(1)); + uint8_t last_led_on = static_cast(ceil(timer_ratio)) - 1; + for (int i = 0; i < 24; i++) { + float brightness_dip = ( i == id(global_led_animation_index) % 24 && i != last_led_on ) ? 0.9f : 1.0f ; + if (i <= timer_ratio) { + it[i] = color * min (255.0f * brightness_dip * (timer_ratio - i) , 255.0f * brightness_dip) ; + } else { + it[i] = Color::BLACK; + } + } + if (id(master_mute_switch).state) { + it[2] = Color::BLACK; + it[3] = muted_color; + it[4] = Color::BLACK; + it[8] = Color::BLACK; + it[9] = muted_color; + it[10] = Color::BLACK; + } + id(global_led_animation_index) = (24 + id(global_led_animation_index) - 1) % 24; + - addressable_rainbow: + name: "Rainbow" + width: 24 + - addressable_lambda: + name: "Tick" + update_interval: 333ms + lambda: |- + static uint8_t index = 0; + Color color(255, 0, 0); + if (initial_run) { + index = 0; + } + for (int i = 0; i < 24; i++) { + if (i <= index ) { + it[i] = Color::BLACK; + } else { + it[i] = color; + } + } + index = (index + 1) % 24; + - addressable_lambda: + name: "Factory Reset Coming Up" + update_interval: 500ms + lambda: |- + static uint8_t index = 0; + Color color(255, 0, 0); + if (initial_run) { + index = 0; + } + for (int i = 0; i < 24; i++) { + if (i <= index ) { + it[i] = color; + } else { + it[i] = Color::BLACK; + } + } + index = (index + 1) % 24; + - addressable_lambda: + name: "Jack Plugged" + update_interval: 40ms + lambda: |- + static uint8_t index = 0; + if (initial_run) { + index = 0; + } + auto light_color = id(led_ring).current_values; + Color color(light_color.get_red() * 255, light_color.get_green() * 255, + light_color.get_blue() * 255); + if (index <= 12) { + for (int i = 0; i < 24; i++) { + if (i == index) { + it[i] = color; + } else if (i == (24 - index) % 24) { + it[i] = color; + } else { + it[i] = Color::BLACK; + } + } + } + index = (index + 1); + - addressable_lambda: + name: "Jack Unplugged" + update_interval: 40ms + lambda: |- + static uint8_t index = 0; + if (initial_run) { + index = 0; + } + auto light_color = id(led_ring).current_values; + Color color(light_color.get_red() * 255, light_color.get_green() * 255, + light_color.get_blue() * 255); + if (index <= 12) { + for (int i = 0; i < 24; i++) { + if (i == 12 - index) { + it[i] = color; + } else if (i == (12 + index) % 24) { + it[i] = color; + } else { + it[i] = Color::BLACK; + } + } + } + index = (index + 1); + + + +script: + # Master script controlling the LEDs, based on different conditions : initialization in progress, wifi and api connected and voice assistant phase. + # For the sake of simplicity and re-usability, the script calls child scripts defined below. + # This script will be called every time one of these conditions is changing. + - id: control_leds + then: + - lambda: | + id(check_if_timers_active).execute(); + if (id(is_timer_active)){ + id(fetch_first_active_timer).execute(); + } + if (id(improv_ble_in_progress)) { + id(control_leds_improv_ble_state).execute(); + } else if (id(init_in_progress)) { + id(control_leds_init_state).execute(); + } else if (!id(wifi_id).is_connected() || !id(api_id).is_connected()){ + id(control_leds_no_ha_connection_state).execute(); + } else if (id(volume_buttons_touched)) { + id(control_leds_volume_buttons_touched).execute(); + } else if (id(btn_action).state) { + id(control_leds_action_button_touched).execute(); + } else if (id(warning)) { + id(control_leds_warning).execute(); + } else if (id(timer_ringing).state) { + id(control_leds_timer_ringing).execute(); + } else if (id(voice_assistant_phase) == ${voice_assist_waiting_for_command_phase_id}) { + id(control_leds_voice_assistant_waiting_for_command_phase).execute(); + } else if (id(voice_assistant_phase) == ${voice_assist_listening_for_command_phase_id}) { + id(control_leds_voice_assistant_listening_for_command_phase).execute(); + } else if (id(voice_assistant_phase) == ${voice_assist_thinking_phase_id}) { + id(control_leds_voice_assistant_thinking_phase).execute(); + } else if (id(voice_assistant_phase) == ${voice_assist_replying_phase_id}) { + id(control_leds_voice_assistant_replying_phase).execute(); + } else if (id(voice_assistant_phase) == ${voice_assist_error_phase_id}) { + id(control_leds_voice_assistant_error_phase).execute(); + } else if (id(voice_assistant_phase) == ${voice_assist_not_ready_phase_id}) { + id(control_leds_voice_assistant_not_ready_phase).execute(); + } else if (id(is_timer_active)) { + id(control_leds_timer_ticking).execute(); + } else if (id(master_mute_switch).state) { + id(control_leds_muted_or_silent).execute(); + } else if (id(nabu_media_player).volume == 0.0f || id(nabu_media_player).is_muted()) { + id(control_leds_muted_or_silent).execute(); + } else if (id(voice_assistant_phase) == ${voice_assist_idle_phase_id}) { + id(control_leds_voice_assistant_idle_phase).execute(); + } + + # Script executed during Improv BLE + # Warm White Twinkle + - id: control_leds_improv_ble_state + then: + - light.turn_on: + brightness: 66% + red: 100% + green: 89% + blue: 71% + id: voice_assistant_leds + effect: "Twinkle" + + # Script executed during initialization + # Blue Twinkle if Wifi is connected, Else solid warm white + - id: control_leds_init_state + then: + - if: + condition: + wifi.connected: + then: + - light.turn_on: + brightness: 66% + red: 9.4% + green: 73.3% + blue: 94.9% + id: voice_assistant_leds + effect: "Twinkle" + else: + - light.turn_on: + brightness: 66% + red: 100% + green: 89% + blue: 71% + id: voice_assistant_leds + effect: "none" + + # Script executed when the device has no connection to Home Assistant + # Red Twinkle (This will be visible during HA updates for example) + - id: control_leds_no_ha_connection_state + then: + - light.turn_on: + brightness: 66% + red: 1 + green: 0 + blue: 0 + id: voice_assistant_leds + effect: "Twinkle" + + # Script executed when the voice assistant is idle (waiting for a wake word) + # Nothing (Either LED ring off or LED ring on if the user decided to turn the user facing LED ring on) + - id: control_leds_voice_assistant_idle_phase + then: + - light.turn_off: voice_assistant_leds + - if: + condition: + light.is_on: led_ring + then: + light.turn_on: led_ring + + # Script executed when the voice assistant is waiting for a command (After the wake word) + # Slow clockwise spin of the LED ring. + - id: control_leds_voice_assistant_waiting_for_command_phase + then: + - light.turn_on: + brightness: !lambda return max( id(led_ring).current_values.get_brightness() , 0.2f ); + id: voice_assistant_leds + effect: "Waiting for Command" + + # Script executed when the voice assistant is listening to a command + # Fast clockwise spin of the LED ring. + - id: control_leds_voice_assistant_listening_for_command_phase + then: + - light.turn_on: + brightness: !lambda return max( id(led_ring).current_values.get_brightness() , 0.2f ); + id: voice_assistant_leds + effect: "Listening For Command" + + # Script executed when the voice assistant is thinking to a command + # The spin stops and the 2 LEDs that are currently on and blinking indicating the commend is being processed. + - id: control_leds_voice_assistant_thinking_phase + then: + - light.turn_on: + brightness: !lambda return max( id(led_ring).current_values.get_brightness() , 0.2f ); + id: voice_assistant_leds + effect: "Thinking" + + # Script executed when the voice assistant is thinking to a command + # Fast anticlockwise spin of the LED ring. + - id: control_leds_voice_assistant_replying_phase + then: + - light.turn_on: + brightness: !lambda return max( id(led_ring).current_values.get_brightness() , 0.2f ); + id: voice_assistant_leds + effect: "Replying" + + # Script executed when the voice assistant is in error + # Fast Red Pulse + - id: control_leds_voice_assistant_error_phase + then: + - light.turn_on: + brightness: !lambda return min ( max( id(led_ring).current_values.get_brightness() , 0.2f ) + 0.1f , 1.0f ); + red: 1 + green: 0 + blue: 0 + id: voice_assistant_leds + effect: "Error" + - delay: 2s + - lambda: id(volume_buttons_touched) = false; + - script.execute: control_leds + + # Script executed when the voice assistant requires user action + # 5 Fast Red Pulses + - id: control_leds_warning + then: + - light.turn_on: + brightness: !lambda return min ( max( id(led_ring).current_values.get_brightness() , 0.2f ) + 0.1f , 1.0f ); + red: 1 + green: 0 + blue: 0 + id: voice_assistant_leds + effect: "Warning" + - delay: 2s + - lambda: id(warning) = false; + - script.execute: control_leds + + # Script executed when the voice assistant is muted or silent + # The LED next to the 2 microphones turn red / one red LED next to the speaker grill + - id: control_leds_muted_or_silent + then: + - light.turn_on: + brightness: !lambda return min ( max( id(led_ring).current_values.get_brightness() , 0.2f ) + 0.1f , 1.0f ); + id: voice_assistant_leds + effect: "Muted or Silent" + + # Script executed when the voice assistant is not ready + - id: control_leds_voice_assistant_not_ready_phase + then: + - light.turn_on: + brightness: 66% + red: 1 + green: 0 + blue: 0 + id: voice_assistant_leds + effect: "Twinkle" + + # Script executed when the volume button is touched + # A number of LEDs turn on indicating a visual representation of the volume of the media player entity. + - id: control_leds_volume_buttons_touched + then: + - light.turn_on: + brightness: !lambda return min ( max( id(led_ring).current_values.get_brightness() , 0.2f ) + 0.1f , 1.0f ); + id: voice_assistant_leds + effect: "Volume Display" + + # Script executed when the jack has just been unplugged + # A ripple effect + - id: control_leds_jack_unplugged_recently + then: + - light.turn_on: + brightness: !lambda return min ( max( id(led_ring).current_values.get_brightness() , 0.2f ) + 0.1f , 1.0f ); + id: voice_assistant_leds + effect: "Jack Unplugged" + + # Script executed when the jack has just been plugged + # A ripple effect + - id: control_leds_jack_plugged_recently + then: + - light.turn_on: + brightness: !lambda return max( id(led_ring).current_values.get_brightness() , 0.2f ); + id: voice_assistant_leds + effect: "Jack Plugged" + + # Script executed when the center button is touched + # The complete LED ring turns on + - id: control_leds_action_button_touched + then: + - light.turn_on: + brightness: !lambda return min ( max( id(led_ring).current_values.get_brightness() , 0.2f ) + 0.1f , 1.0f ); + id: voice_assistant_leds + effect: "Action Button Touched" + + # Script executed when the timer is ringing, to control the LEDs + # The LED ring blinks. + - id: control_leds_timer_ringing + then: + - light.turn_on: + brightness: !lambda return min ( max( id(led_ring).current_values.get_brightness() , 0.2f ) + 0.1f , 1.0f ); + id: voice_assistant_leds + effect: "Timer Ring" + + # Script executed when the timer is ticking, to control the LEDs + # The LEDs shows the remaining time as a fraction of the full ring. + - id: control_leds_timer_ticking + then: + - light.turn_on: + brightness: !lambda return min ( max( id(led_ring).current_values.get_brightness() , 0.2f ) + 0.1f , 1.0f ); + id: voice_assistant_leds + effect: "Timer tick" + + diff --git a/config/common/media_player.yaml b/config/common/media_player.yaml new file mode 100644 index 00000000..ca52d8df --- /dev/null +++ b/config/common/media_player.yaml @@ -0,0 +1,127 @@ + +audio_dac: + - platform: pcm5122 + address: 0x4D + +fusb302b: + id: pd_fusb302b + irq_pin: GPIO1 + request_voltage: 9 + on_power_ready: + then: + - logger.log: + format: "PD contract got accepted: %s" + args: [ 'id(pd_fusb302b).contract.c_str()' ] + + - text_sensor.template.publish: + id: pd_state_text + state: !lambda 'return id(pd_fusb302b).contract;' + on_disconnect: + - text_sensor.template.publish: + id: pd_state_text + state: "Disconnected" + + +media_player: + - platform: satellite1 + id: nabu_media_player + name: Sat1 Media Player + internal: false + sample_rate: 48000 + i2s_dout_pin: GPIO9 + bits_per_sample: 32bit + i2s_clock_mode: external + channel: right_left + i2s_audio_id: i2s_shared + volume_increment: 0.05 + volume_min: 0.4 + volume_max: 0.85 + on_mute: + - script.execute: control_leds + on_unmute: + - script.execute: control_leds + on_volume: + - script.execute: control_leds + on_announcement: + - nabu.set_ducking: + decibel_reduction: 20 + duration: 0.0s + on_state: + if: + condition: + and: + - switch.is_off: timer_ringing + - not: + voice_assistant.is_running: + - not: + lambda: return id(nabu_media_player)->state == media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING; + then: + - nabu.set_ducking: + decibel_reduction: 0 + duration: 1.0s + + files: + - id: center_button_press_sound + file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/center_button_press.flac + - id: center_button_double_press_sound + file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/center_button_double_press.flac + - id: center_button_triple_press_sound + file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/center_button_triple_press.flac + - id: center_button_long_press_sound + file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/center_button_long_press.flac + - id: factory_reset_initiated_sound + file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/factory_reset_initiated.mp3 + - id: factory_reset_cancelled_sound + file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/factory_reset_cancelled.mp3 + - id: mute_switch_on_sound + file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/mute_switch_on.flac + - id: mute_switch_off_sound + file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/mute_switch_off.flac + - id: timer_finished_sound + file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/timer_finished.flac + - id: wake_word_triggered_sound + file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/wake_word_triggered.flac + + + +script: + # Script executed when we want to play sounds on the device. + - id: play_sound + parameters: + priority: bool + sound_file: "media_player::MediaFile*" + then: + - lambda: |- + if (priority) { + id(nabu_media_player) + ->make_call() + .set_command(media_player::MediaPlayerCommand::MEDIA_PLAYER_COMMAND_STOP) + .set_announcement(true) + .perform(); + } + if ( (id(nabu_media_player).state != media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING ) || priority) { + id(nabu_media_player) + ->make_call() + .set_announcement(true) + .set_local_media_file(sound_file) + .perform(); + } + + # Script to increased/decreased volume + - id: control_volume + mode: restart + parameters: + increase_volume: bool # True: Increase volume / False: Decrease volume. + then: + - delay: 16ms + - if: + condition: + lambda: return increase_volume; + then: + - media_player.volume_up: + else: + - media_player.volume_down: + - script.execute: control_leds + - delay: 2s + - lambda: id(volume_buttons_touched) = false; + - script.execute: control_leds \ No newline at end of file diff --git a/config/common/mmwave_ld2410.yaml b/config/common/mmwave_ld2410.yaml new file mode 100644 index 00000000..3ac77f35 --- /dev/null +++ b/config/common/mmwave_ld2410.yaml @@ -0,0 +1,20 @@ +ld2410: + +uart: + tx_pin: GPIO43 + rx_pin: GPIO44 + baud_rate: 256000 + parity: NONE + stop_bits: 1 + +binary_sensor: + - platform: ld2410 + has_target: + name: Room Presence + id: room_presence + has_moving_target: + name: Moving Target + has_still_target: + name: Still Target + out_pin_presence_status: + name: out pin presence status \ No newline at end of file diff --git a/config/common/timer.yaml b/config/common/timer.yaml new file mode 100644 index 00000000..d0cb6526 --- /dev/null +++ b/config/common/timer.yaml @@ -0,0 +1,103 @@ +globals: + # Global variable storing the first active timer + - id: first_active_timer + type: voice_assistant::Timer + restore_value: false + # Global variable storing if a timer is active + - id: is_timer_active + type: bool + restore_value: false + + +switch: + # Internal switch to track when a timer is ringing on the device. + - platform: template + id: timer_ringing + optimistic: true + internal: true + restore_mode: ALWAYS_OFF + on_turn_off: + # Disable stop wake word + # - lambda: id(stop).disable(); + # Stop any current annoucement (ie: stop the timer ring mid playback) + - if: + condition: + lambda: return id(nabu_media_player)->state == media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING; + then: + lambda: |- + id(nabu_media_player) + ->make_call() + .set_command(media_player::MediaPlayerCommand::MEDIA_PLAYER_COMMAND_STOP) + .set_announcement(true) + .perform(); + # Set back ducking ratio to zero + - nabu.set_ducking: + decibel_reduction: 0 + duration: 1.0s + # Refresh the LED ring + - script.execute: control_leds + on_turn_on: + # Duck audio + - nabu.set_ducking: + decibel_reduction: 20 + duration: 0.0s + # Enable stop wake word + # - lambda: id(stop).enable(); + # Ring timer + - script.execute: ring_timer + # Refresh LED + - script.execute: control_leds + # If 15 minutes have passed and the timer is still ringing, stop it. + - delay: 15min + - switch.turn_off: timer_ringing + + +script: + # Script executed when the timer is ringing, to playback sounds. + - id: ring_timer + then: + - while: + condition: + switch.is_on: timer_ringing + then: + - script.execute: + id: play_sound + priority: true + sound_file: !lambda return id(timer_finished_sound); + - wait_until: + lambda: |- + return id(nabu_media_player)->state == media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING; + - wait_until: + not: + lambda: |- + return id(nabu_media_player)->state == media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING; + + + # Script used to fetch the first active timer (Stored in global first_active_timer) + - id: fetch_first_active_timer + then: + - lambda: | + const auto timers = id(va).get_timers(); + auto output_timer = timers.begin()->second; + for (auto &iterable_timer : timers) { + if (iterable_timer.second.is_active && iterable_timer.second.seconds_left <= output_timer.seconds_left) { + output_timer = iterable_timer.second; + } + } + id(first_active_timer) = output_timer; + + # Script used to check if a timer is active (Stored in global is_timer_active) + - id: check_if_timers_active + then: + - lambda: | + const auto timers = id(va).get_timers(); + bool output = false; + if (timers.size() > 0) { + for (auto &iterable_timer : timers) { + if(iterable_timer.second.is_active) { + output = true; + } + } + } + id(is_timer_active) = output; + diff --git a/config/common/voice_assistant.yaml b/config/common/voice_assistant.yaml new file mode 100644 index 00000000..0befdb8c --- /dev/null +++ b/config/common/voice_assistant.yaml @@ -0,0 +1,188 @@ +substitutions: + # Phases of the Voice Assistant + # The voice assistant is ready to be triggered by a wake word + voice_assist_idle_phase_id: '1' + # The voice assistant is waiting for a voice command (after being triggered by the wake word) + voice_assist_waiting_for_command_phase_id: '2' + # The voice assistant is listening for a voice command + voice_assist_listening_for_command_phase_id: '3' + # The voice assistant is currently processing the command + voice_assist_thinking_phase_id: '4' + # The voice assistant is replying to the command + voice_assist_replying_phase_id: '5' + # The voice assistant is not ready + voice_assist_not_ready_phase_id: '10' + # The voice assistant encountered an error + voice_assist_error_phase_id: '11' + +globals: + # Global variable tracking the phase of the voice assistant (defined above). Initialized to not_ready + - id: voice_assistant_phase + type: int + restore_value: no + # initial_value: ${voice_assist_not_ready_phase_id} + +microphone: + - platform: satellite1 + sample_rate: 48000 + i2s_din_pin: GPIO15 + bits_per_sample: 32bit + i2s_clock_mode: external + channel: right_left + pdm: false + + i2s_audio_id: i2s_shared + channel_0: + id: asr_mic + amplify_shift: 0 + channel_1: + id: comm_mic + amplify_shift: 6 + + + +micro_wake_word: + id: mww + models: + - model: hey_jarvis + id: hey_jarvis + - model: okay_nabu + id: okay_nabu + - model: https://github.com/kahrendt/microWakeWord/releases/download/stop/stop.json + id: stop + internal: true + vad: + microphone: comm_mic + + on_wake_word_detected: + # If the wake word is detected when the device is muted (Possible with the software mute switch): Do nothing + - if: + condition: + switch.is_off: master_mute_switch + then: + # If a timer is ringing: Stop it, do not start the voice assistant (We can stop timer from voice!) + - if: + condition: + switch.is_on: timer_ringing + then: + - switch.turn_off: timer_ringing + # Start voice assistant, stop current announcement. + else: + - if: + condition: + lambda: return id(nabu_media_player)->state == media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING; + then: + lambda: |- + id(nabu_media_player) + ->make_call() + .set_command(media_player::MediaPlayerCommand::MEDIA_PLAYER_COMMAND_STOP) + .set_announcement(true) + .perform(); + else: + - if: + condition: + switch.is_on: wake_sound + then: + - script.execute: + id: play_sound + priority: true + sound_file: !lambda return id(wake_word_triggered_sound); + - delay: 300ms + - voice_assistant.start: + wake_word: !lambda return wake_word; + + + +voice_assistant: + id: va + microphone: asr_mic + media_player: nabu_media_player + micro_wake_word: mww + use_wake_word: false + noise_suppression_level: 0 + auto_gain: 0 dbfs + volume_multiplier: 1 + on_client_connected: + - lambda: id(init_in_progress) = false; + - micro_wake_word.start: + - lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id}; + - script.execute: control_leds + on_client_disconnected: + - voice_assistant.stop: + - lambda: id(voice_assistant_phase) = ${voice_assist_not_ready_phase_id}; + - script.execute: control_leds + on_error: + - if: + condition: + lambda: return !id(init_in_progress); + then: + - lambda: id(voice_assistant_phase) = ${voice_assist_error_phase_id}; + - script.execute: control_leds + # When the voice assistant starts: Play a wake up sound, duck audio. + on_start: + - nabu.set_ducking: + decibel_reduction: 20 # Number of dB quieter; higher implies more quiet, 0 implies full volume + duration: 0.0s # The duration of the transition (default is 0) + on_listening: + - lambda: id(voice_assistant_phase) = ${voice_assist_waiting_for_command_phase_id}; + - script.execute: control_leds + on_stt_vad_start: + - lambda: id(voice_assistant_phase) = ${voice_assist_listening_for_command_phase_id}; + - script.execute: control_leds + on_stt_vad_end: + - lambda: id(voice_assistant_phase) = ${voice_assist_thinking_phase_id}; + - script.execute: control_leds + on_tts_start: + - lambda: id(voice_assistant_phase) = ${voice_assist_replying_phase_id}; + - script.execute: control_leds + # Start a script that would potentially enable the stop word if the response is longer than a second + - script.execute: activate_stop_word_if_tts_step_is_long + # When the voice assistant ends ... + on_end: + - wait_until: + not: + voice_assistant.is_running: + # Stop ducking audio. + - nabu.set_ducking: + decibel_reduction: 0 # 0 dB means no reduction + duration: 1.0s + # Stop the script that would potentially enable the stop word if the response is longer than a second + - script.stop: activate_stop_word_if_tts_step_is_long + # Disable the stop word (If the tiemr is not ringing) + - if: + condition: + switch.is_off: timer_ringing + then: + # - lambda: id(stop).disable(); + # If the end happened because of an error, let the error phase on for a second + - if: + condition: + lambda: return id(voice_assistant_phase) == ${voice_assist_error_phase_id}; + then: + - delay: 1s + # Reset the voice assistant phase id and reset the LED animations. + - lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id}; + - script.execute: control_leds + + on_timer_finished: + - switch.turn_on: timer_ringing + on_timer_started: + - script.execute: control_leds + on_timer_cancelled: + - script.execute: control_leds + on_timer_updated: + - script.execute: control_leds + on_timer_tick: + - script.execute: control_leds + + +script: + # Script used activate the stop word if the TTS step is long. + # Why is this wrapped on a script? + # Becasue we want to stop the sequence if the TTS step is faster than that. + # This allows us to prevent having the deactivation of the stop word before its own activation. + - id: activate_stop_word_if_tts_step_is_long + then: + - delay: 1s + # Enable stop wake word + # - lambda: id(stop).enable(); \ No newline at end of file diff --git a/config/common/wifi_credentials.yaml b/config/common/wifi_credentials.yaml new file mode 100644 index 00000000..d7d72276 --- /dev/null +++ b/config/common/wifi_credentials.yaml @@ -0,0 +1,8 @@ +wifi: + ssid: !secret wifi_ssid + password: !secret wifi_password + fast_connect: true + + ap: + ssid: "${node_name}" + password: !secret wifi_ap_password diff --git a/config/common/wifi_improv.yaml b/config/common/wifi_improv.yaml new file mode 100644 index 00000000..f3f0744d --- /dev/null +++ b/config/common/wifi_improv.yaml @@ -0,0 +1,17 @@ +globals: + - id: improv_ble_in_progress + type: bool + restore_value: no + initial_value: 'false' + +wifi: + id: wifi_id + on_connect: + - delay: 5s # Gives time for improv results to be transmitted + - lambda: id(improv_ble_in_progress) = false; + - script.execute: control_leds + on_disconnect: + - script.execute: control_leds + +improv_serial: + diff --git a/config/satellite1.factory.yaml b/config/satellite1.factory.yaml new file mode 100644 index 00000000..fde861ee --- /dev/null +++ b/config/satellite1.factory.yaml @@ -0,0 +1,81 @@ +substitutions: + company_name: FutureProofHomes + project_name: Satellite1 + + #set this version by GHA at build time + esp32_fw_version: dev + +packages: + # This is an inline package to prefix the on_client_connected with the wait_until action + # It must appear before the actual package so it becomes the orignal config and the + # on_client_connected list from the package config is appended onto this one. + va_connected_wait_for_ble: + voice_assistant: + on_client_connected: + - wait_until: + not: ble.enabled + - delay: 2s + wifi: + on_disconnect: + - ble.enable: + home-assistant-voice: !include satellite1.yaml + +esphome: + project: + name: ${company_name}.${project_name} + version: dev + +ota: + - platform: http_request + id: ota_http_request + +http_request: + +update: + - platform: http_request + name: None + id: update_http_request + # source: https://firmware.esphome.io/home-assistant-voice-pe/home-assistant-voice/manifest.json + source: https://raw.githubusercontent.com/FutureProofHomes/Documentation/refs/heads/main/manifest.json + +dashboard_import: + # package_import_url: github://esphome/home-assistant-voice-pe/home-assistant-voice.yaml + package_import_url: github://FutureProofHomes/Satellite1-ESPHome/config/satellite1.yaml + +wifi: + on_connect: + - delay: 5s # Gives time for improv results to be transmitted + - ble.disable: + - script.execute: control_leds + +improv_serial: + +esp32_improv: + authorizer: btn_action + on_start: + - lambda: id(improv_ble_in_progress) = true; + - script.execute: control_leds + on_provisioned: + - lambda: id(improv_ble_in_progress) = false; + - script.execute: control_leds + on_stop: + - lambda: id(improv_ble_in_progress) = false; + - script.execute: control_leds + +switch: + - platform: template + id: beta_firmware + name: Beta firmware + icon: "mdi:test-tube" + disabled_by_default: true + entity_category: diagnostic + optimistic: true + restore_mode: RESTORE_DEFAULT_OFF + on_turn_on: + - logger.log: "OTA updates set to use Beta firmware" + # - lambda: id(update_http_request).set_source_url("https://firmware.esphome.io/home-assistant-voice-pe/home-assistant-voice/manifest-beta.json"); + - lambda: id(update_http_request).set_source_url("https://raw.githubusercontent.com/FutureProofHomes/Documentation/refs/heads/main/manifest-beta.json"); + on_turn_off: + - logger.log: "OTA updates set to use Production firmware" + # - lambda: id(update_http_request).set_source_url("https://firmware.esphome.io/home-assistant-voice-pe/home-assistant-voice/manifest.json"); + - lambda: id(update_http_request).set_source_url("https://raw.githubusercontent.com/FutureProofHomes/Documentation/refs/heads/main/manifest.json"); \ No newline at end of file diff --git a/config/satellite1.yaml b/config/satellite1.yaml new file mode 100644 index 00000000..ab3ed729 --- /dev/null +++ b/config/satellite1.yaml @@ -0,0 +1,142 @@ +substitutions: + #Change to any preferred name + friendly_name: "Satellite1" + + #Change to calibrate your temperature/humidity sensor readings + temp_offset: "-3" + humidity_offset: "5" + + #Recommend leaving the following unchanged + node_name: satellite1 + company_name: FutureProofHomes + project_name: Satellite1 + component_name: Core + + esp32_fw_version: "v2.0.0-alpha.8" + xmos_fw_version: "v1.0.1-alpha.33" + built_for_core_hw_version: "v1.0.0-beta.1" + built_for_hat_hw_version: "v1.0.0-beta.1" + + +globals: + # Global initialisation variable. Initialized to true and set to false once everything is connected. Only used to have a smooth "plugging" experience + - id: init_in_progress + type: bool + restore_value: no + initial_value: 'true' + # Global variable storing if user action causes warning + - id: warning + type: bool + restore_value: no + initial_value: 'false' + # Global variable tracking if the XMOS flash button was recently touched. + - id: xmos_flashing_started + type: bool + restore_value: no + initial_value: 'false' + + +esphome: + name: ${node_name} + name_add_mac_suffix: true + friendly_name: ${friendly_name} + min_version: 2024.11.2 + + project: + name: ${company_name}.${project_name} + version: dev + + on_boot: + - priority: 375 + then: + # Run the script to refresh the LED status + - script.execute: control_leds + - delay: 1s + + # If after 10 minutes, the device is still initializing (It did not yet connect to Home Assistant), turn off the init_in_progress variable and run the script to refresh the LED status + - delay: 10min + - if: + condition: + lambda: return id(init_in_progress); + then: + - lambda: id(init_in_progress) = false; + - script.execute: control_leds + + - priority: 600 + then: + - logger.log: "${company_name} ${project_name} ${component_name} version ${built_for_core_hw_version} running ESP firmware: ${esp32_fw_version} and XMOS firmware: ${xmos_fw_version}" + - delay: 30s + - if: + condition: + lambda: return id(init_in_progress); + then: + - lambda: id(init_in_progress) = false; + + +logger: + deassert_rts_dtr: true + hardware_uart : USB_SERIAL_JTAG + level: debug + + +dashboard_import: + package_import_url: github://futureproofhomes/satellite1-esphome/config/satellite1.yaml + + +external_components: + - source: + type: git + url: https://github.com/gnumpi/nabu-voice-kit + ref: dev + components: [ audio_dac, media_player ] + + - source: + type: local + path: ../esphome/components + components: [ i2s_audio, satellite1, pcm5122, fusb302b ] + + - source: + type: git + url: https://github.com/esphome/voice-kit + ref: 24.10.17 + components: [ microphone, micro_wake_word, voice_assistant ] + + +packages: + core_board: !include common/core_board.yaml + wifi: !include common/wifi_improv.yaml + sensors: !include common/hat_sensors.yaml + buttons: !include common/buttons.yaml + ha: !include common/home_assistant.yaml + mp: !include common/media_player.yaml + va: !include common/voice_assistant.yaml + timer: !include common/timer.yaml + led_ring: !include common/led_ring.yaml + + #OPTIONAL COMPONENTS + # mmwave_ld2410: !include common/mmwave_ld2410.yaml + # debug: !include common/debug.yaml + +ota: + - platform: satellite1 + id: ota_satellite1_xmos + + - platform: esphome + id: ota_esphome + +http_request: + +safe_mode: + +status_led: + pin: + number: GPIO45 + ignore_strapping_warning: true + + +satellite1: + spi_id: spi_0 + cs_pin: GPIO10 + data_rate: 8000000 + spi_mode: MODE3 + xmos_rst_pin: GPIO4 \ No newline at end of file diff --git a/config/satellite_mic_test.yaml b/config/satellite_mic_test.yaml deleted file mode 100644 index f9c34394..00000000 --- a/config/satellite_mic_test.yaml +++ /dev/null @@ -1,198 +0,0 @@ -substitutions: - node_name: home-x-satellite1 - friendly_name: 'HomeX Satellite' - player_name: 'Satellite Speaker' - area: media-room - - # Phases of the Voice Assistant - # IDLE: The voice assistant is ready to be triggered by a wake-word - voice_assist_idle_phase_id: '1' - # LISTENING: The voice assistant is ready to listen to a voice command (after being triggered by the wake word) - voice_assist_listening_phase_id: '2' - # THINKING: The voice assistant is currently processing the command - voice_assist_thinking_phase_id: '3' - # REPLYING: The voice assistant is replying to the command - voice_assist_replying_phase_id: '4' - # NOT_READY: The voice assistant is not ready - voice_assist_not_ready_phase_id: '10' - # ERROR: The voice assistant encountered an error - voice_assist_error_phase_id: '11' - # MUTED: The voice assistant is muted and will not reply to a wake-word - voice_assist_muted_phase_id: '12' - - -globals: - # Global initialisation variable. Initialized to true and set to false once everything is connected. Only used to have a smooth "plugging" experience - - id: init_in_progress - type: bool - restore_value: no - initial_value: 'true' - - - id: voice_assistant_phase - type: int - restore_value: no - initial_value: ${voice_assist_not_ready_phase_id} - -external_components: - - source: - type: git - url: https://github.com/gnumpi/esphome_audio - ref: allow_external_clk_mode - - components: [ adf_pipeline, i2s_audio ] - - -esphome: - name: ${node_name} - min_version: 2024.2.0 - platformio_options: - board_build.flash_mode: dio - board_upload.maximum_size: 16777216 - #board_build.partitions: "../../../esp32-s3/custom_partitions_16MB.csv" - on_boot: - priority: 600 - then: - # Run the script to refresh the LED status - # If after 30 seconds, the device is still initializing (It did not yet connect to Home Assistant), turn off the init_in_progress variable and run the script to refresh the LED status - - delay: 30s - - if: - condition: - lambda: return id(init_in_progress); - then: - - lambda: id(init_in_progress) = false; - - -esp32: - board: esp32-s3-devkitc-1 - variant: ESP32S3 - flash_size: 16MB - framework: - type: esp-idf - version: recommended - sdkconfig_options: - # need to set a s3 compatible board for the adf-sdk to compile - # board specific code is not used though - CONFIG_ESP32_S3_BOX_BOARD: "y" - CONFIG_ESP32_WIFI_STATIC_RX_BUFFER_NUM: "16" - CONFIG_ESP32_WIFI_DYNAMIC_RX_BUFFER_NUM: "512" - CONFIG_TCPIP_RECVMBOX_SIZE: "512" - - -api: - -logger: - hardware_uart : UART0 - level: VERBOSE - -psram: - mode: octal - speed: 120MHz - -ota: - platform: esphome - -wifi: - ap: - on_connect: - - delay: 5s # Gives time for improv results to be transmitted - - ble.disable: - on_disconnect: - - ble.enable: - -improv_serial: - -esp32_improv: - authorizer: none - -#I2S_BCLK IO17 -#I2S_LRCK IO45 -#I2S_DIN IO15 -#I2S_DOUT IO16 -#PI_MCLK IO2 - - -i2s_audio: - - id: i2s_in - i2s_lrclk_pin: GPIO45 - i2s_bclk_pin: GPIO17 - - -adf_pipeline: - - platform: i2s_audio - type: audio_in - id: adf_i2s_in - i2s_audio_id: i2s_in - i2s_clock_mode: external - i2s_din_pin: GPIO15 - pdm: false - channel: left - sample_rate: 48000 - bits_per_sample: 32bit - - -microphone: - - platform: adf_pipeline - id: adf_microphone - gain_log2: 3 - keep_pipeline_alive: false - pipeline: - - adf_i2s_in - - resampler - - self - - -voice_assistant: - microphone: adf_microphone - #speaker: i2s_speaker - use_wake_word: False - - noise_suppression_level: 4 - auto_gain: 31dBFS - volume_multiplier: 8.0 - - on_client_connected: - - lambda: id(init_in_progress) = false; - - if: - condition: - switch.is_on: stream_to_va - then: - - voice_assistant.start_continuous: - - lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id}; - else: - - lambda: id(voice_assistant_phase) = ${voice_assist_muted_phase_id}; - - on_client_disconnected: - - lambda: id(voice_assistant_phase) = ${voice_assist_not_ready_phase_id}; - - voice_assistant.stop - - -switch: - - platform: template - name: Stream Mic To VA - id: stream_to_va - optimistic: true - restore_mode: RESTORE_DEFAULT_ON - icon: mdi:assistant - # When the switch is turned on (on Home Assistant): - # Start the voice assistant component - on_turn_on: - - if: - condition: - lambda: return !id(init_in_progress); - then: - - lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id}; - - if: - condition: - not: - - voice_assistant.is_running - then: - - voice_assistant.start_continuous - on_turn_off: - - if: - condition: - lambda: return !id(init_in_progress); - then: - - voice_assistant.stop - - lambda: id(voice_assistant_phase) = ${voice_assist_muted_phase_id}; - - diff --git a/esphome/components/fusb302b/__init__.py b/esphome/components/fusb302b/__init__.py new file mode 100644 index 00000000..586fce83 --- /dev/null +++ b/esphome/components/fusb302b/__init__.py @@ -0,0 +1,112 @@ +from esphome import automation +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c + + +from esphome.pins import internal_gpio_input_pin_number + +from esphome.const import ( + CONF_ID, + CONF_IRQ_PIN, + CONF_ON_CONNECT, + CONF_ON_DISCONNECT, + CONF_ON_ERROR, + CONF_TRIGGER_ID +) + +CODEOWNERS = ["@gnumpi"] +DEPENDENCIES = ["i2c"] + +pd_ns = cg.esphome_ns.namespace("power_delivery") +PowerDelivery = pd_ns.class_("PowerDelivery", cg.Component) +FUSB302B = pd_ns.class_("FUSB302B", PowerDelivery, cg.Component, i2c.I2CDevice) + + +RequestVoltageAction = pd_ns.class_("PowerDeliveryRequestVoltage", automation.Action, cg.Parented.template(PowerDelivery)) + +ConnectedTrigger = pd_ns.class_("ConnectedTrigger", automation.Trigger) +DisconnectedTrigger = pd_ns.class_("DisconnectedTrigger", automation.Trigger.template()) +ErrorTrigger = pd_ns.class_("ErrorTrigger", automation.Trigger.template()) + +TransitionTrigger = pd_ns.class_("TransitionTrigger", automation.Trigger.template()) +PowerReadyTrigger = pd_ns.class_("PowerReadyTrigger", automation.Trigger) + +ValidContractTrigger = pd_ns.class_("ValidContractTrigger", automation.Trigger) +IsConnectedCondition = pd_ns.class_("IsConnectedCondition", automation.Condition) + +CONF_REQUEST_VOLTAGE = "request_voltage" +CONF_ON_CONTRACT = "on_contract" +CONF_ON_PWR_RDY = "on_power_ready" + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(FUSB302B), + cv.Required(CONF_IRQ_PIN): internal_gpio_input_pin_number, + cv.Required(CONF_REQUEST_VOLTAGE): cv.Range(min=5, max=20, max_included=True), + + cv.Optional(CONF_ON_CONNECT): automation.validate_automation({ + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ConnectedTrigger), + }), + cv.Optional(CONF_ON_DISCONNECT): automation.validate_automation({ + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DisconnectedTrigger), + }), + cv.Optional(CONF_ON_ERROR): automation.validate_automation({ + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ErrorTrigger), + }), + cv.Optional(CONF_ON_PWR_RDY): automation.validate_automation({ + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PowerReadyTrigger), + }), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x22)) +) + +PD_ACTION_SCHEMA = automation.maybe_simple_id({cv.GenerateID(): cv.use_id(PowerDelivery)}) + + +@automation.register_action( + "power_delivery.request_voltage", + RequestVoltageAction, + cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(PowerDelivery), + cv.Required(CONF_REQUEST_VOLTAGE): cv.templatable(cv.Range(min=5, max=20, max_included=True)), + }, + key=CONF_REQUEST_VOLTAGE, + ), +) +async def power_delivery_request_voltage_action(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + voltage = await cg.templatable(config[CONF_REQUEST_VOLTAGE], args, int) + cg.add(var.set_voltage(voltage)) + return var + + + +@automation.register_condition( + "power_delivery.is_connected", IsConnectedCondition, PD_ACTION_SCHEMA +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + cg.add( var.set_request_voltage(config[CONF_REQUEST_VOLTAGE])) + cg.add( var.set_irq_pin(config[CONF_IRQ_PIN])) + for conf in config.get(CONF_ON_CONNECT, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_DISCONNECT, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_ERROR, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_PWR_RDY, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) \ No newline at end of file diff --git a/esphome/components/fusb302b/automation.h b/esphome/components/fusb302b/automation.h new file mode 100644 index 00000000..90f6c6a8 --- /dev/null +++ b/esphome/components/fusb302b/automation.h @@ -0,0 +1,69 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "pd.h" + +namespace esphome { +namespace power_delivery { + +template +class PowerDeliveryRequestVoltage : public Action, public Parented { +public: + TEMPLATABLE_VALUE(int, voltage) + void play(Ts... x) override {this->parent_->request_voltage(this->voltage_.value(x...));} +}; + + +class StateTrigger : public Trigger<> { + public: + explicit StateTrigger(PowerDelivery *pd) { + pd->add_on_state_callback([this]() { this->trigger(); }); + } +}; + +template class PDStateTrigger : public Trigger<> { + public: + explicit PDStateTrigger(PowerDelivery *pd) { + pd->add_on_state_callback([this, pd]() { + if (pd->state == State) + this->trigger(); + }); + } +}; + +class PowerReadyTrigger : public Trigger<>{ +public: + explicit PowerReadyTrigger(PowerDelivery *pd) { + pd->add_on_state_callback([this, pd]() { + if (pd->state == PD_STATE_EXPLICIT_SPR_CONTRACT || pd->state == PD_STATE_EXPLICIT_EPR_CONTRACT ) + this->trigger(); + }); + } +}; + +class ConnectedTrigger : public Trigger<>{ +public: + explicit ConnectedTrigger(PowerDelivery *pd) { + pd->add_on_state_callback([this, pd]() { + if( pd->prev_state_ == PD_STATE_DISCONNECTED && pd->state == PD_STATE_DEFAULT_CONTRACT) + this->trigger(); + }); + } +}; + + + +using DisconnectedTrigger = PDStateTrigger; +using ErrorTrigger = PDStateTrigger; +using TransitionTrigger = PDStateTrigger; + +template class IsConnectedCondition : public Condition, public Parented { + public: + bool check(Ts... x) override { return this->parent_->state == PowerDeliveryState::PD_STATE_DEFAULT_CONTRACT || this->parent_->state == PowerDeliveryState::PD_STATE_EXPLICIT_SPR_CONTRACT || PowerDeliveryState::PD_STATE_EXPLICIT_EPR_CONTRACT; } +}; + + + + +} +} diff --git a/esphome/components/fusb302b/fusb302_defines.h b/esphome/components/fusb302b/fusb302_defines.h new file mode 100644 index 00000000..8b53b7b2 --- /dev/null +++ b/esphome/components/fusb302b/fusb302_defines.h @@ -0,0 +1,229 @@ +#pragma once +#include + +/* I2C addresses of the FUSB302B chips */ +#define FUSB302B_ADDR (0x22 << 1) +#define FUSB302B01_ADDR (0x23 << 1) +#define FUSB302B10_ADDR (0x24 << 1) +#define FUSB302B11_ADDR (0x25 << 1) + +/* Device ID register */ +#define FUSB_DEVICE_ID 0x01 +#define FUSB_DEVICE_ID_VERSION_ID_SHIFT 4 +#define FUSB_DEVICE_ID_VERSION_ID (0xF << FUSB_DEVICE_ID_VERSION_ID_SHIFT) +#define FUSB_DEVICE_ID_PRODUCT_ID_SHIFT 2 +#define FUSB_DEVICE_ID_PRODUCT_ID (0x3 << FUSB_DEVICE_ID_PRODUCT_ID_SHIFT) +#define FUSB_DEVICE_ID_REVISION_ID_SHIFT 0 +#define FUSB_DEVICE_ID_REVISION_ID (0x3 << FUSB_DEVICE_ID_REVISION_ID_SHIFT) + +/* Switches0 register */ +#define FUSB_SWITCHES0 0x02 +#define FUSB_SWITCHES0_PU_EN2 (1 << 7) +#define FUSB_SWITCHES0_PU_EN1 (1 << 6) +#define FUSB_SWITCHES0_VCONN_CC2 (1 << 5) +#define FUSB_SWITCHES0_VCONN_CC1 (1 << 4) +#define FUSB_SWITCHES0_MEAS_CC2 (1 << 3) +#define FUSB_SWITCHES0_MEAS_CC1 (1 << 2) +#define FUSB_SWITCHES0_PDWN_2 (1 << 1) +#define FUSB_SWITCHES0_PDWN_1 1 + +/* Switches1 register */ +#define FUSB_SWITCHES1 0x03 +#define FUSB_SWITCHES1_POWERROLE (1 << 7) +#define FUSB_SWITCHES1_SPECREV_SHIFT 5 +#define FUSB_SWITCHES1_SPECREV (0x3 << FUSB_SWITCHES1_SPECREV_SHIFT) +#define FUSB_SWITCHES1_DATAROLE (1 << 4) +#define FUSB_SWITCHES1_AUTO_CRC (1 << 2) +#define FUSB_SWITCHES1_TXCC2 (1 << 1) +#define FUSB_SWITCHES1_TXCC1 1 + +/* Measure register */ +#define FUSB_MEASURE 0x04 +#define FUSB_MEASURE_MEAS_VBUS (1 << 6) +#define FUSB_MEASURE_MDAC_SHIFT 0 +#define FUSB_MEASURE_MDAC (0x3F << FUSB_MEASURE_MDAC_SHIFT) + +/* Slice register */ +#define FUSB_SLICE 0x05 +#define FUSB_SLICE_SDAC_HYS_SHIFT 6 +#define FUSB_SLICE_SDAC_HYS (0x3 << FUSB_SLICE_SDAC_HYS_SHIFT) +#define FUSB_SLICE_SDAC_SHIFT 0 +#define FUSB_SLICE_SDAC (0x3F << FUSB_SLICE_SDAC_SHIFT) + +/* Control0 register */ +#define FUSB_CONTROL0 0x06 +#define FUSB_CONTROL0_TX_FLUSH (1 << 6) +#define FUSB_CONTROL0_INT_MASK (1 << 5) +#define FUSB_CONTROL0_HOST_CUR_SHIFT 2 +#define FUSB_CONTROL0_HOST_CUR (0x3 << FUSB_CONTROL0_HOST_CUR_SHIFT) +#define FUSB_CONTROL0_AUTO_PRE (1 << 1) +#define FUSB_CONTROL0_TX_START 1 + +/* Control1 register */ +#define FUSB_CONTROL1 0x07 +#define FUSB_CONTROL1_ENSOP2DB (1 << 6) +#define FUSB_CONTROL1_ENSOP1DB (1 << 5) +#define FUSB_CONTROL1_BIST_MODE2 (1 << 4) +#define FUSB_CONTROL1_RX_FLUSH (1 << 2) +#define FUSB_CONTROL1_ENSOP2 (1 << 1) +#define FUSB_CONTROL1_ENSOP1 1 + +/* Control2 register */ +#define FUSB_CONTROL2 0x08 +#define FUSB_CONTROL2_TOG_SAVE_PWR_SHIFT 6 +#define FUSB_CONTROL2_TOG_SAVE_PWR (0x3 << FUSB_CONTROL2_TOG_SAVE_PWR) +#define FUSB_CONTROL2_TOG_RD_ONLY (1 << 5) +#define FUSB_CONTROL2_WAKE_EN (1 << 3) +#define FUSB_CONTROL2_MODE_SHIFT 1 +#define FUSB_CONTROL2_MODE (0x3 << FUSB_CONTROL2_MODE_SHIFT) +#define FUSB_CONTROL2_TOGGLE 1 + +/* Control3 register */ +#define FUSB_CONTROL3 0x09 +#define FUSB_CONTROL3_SEND_HARD_RESET (1 << 6) +#define FUSB_CONTROL3_BIST_TMODE (1 << 5) +#define FUSB_CONTROL3_AUTO_HARDRESET (1 << 4) +#define FUSB_CONTROL3_AUTO_SOFTRESET (1 << 3) +#define FUSB_CONTROL3_N_RETRIES_SHIFT 1 +#define FUSB_CONTROL3_N_RETRIES_MASK (0x3 << FUSB_CONTROL3_N_RETRIES_SHIFT) +#define FUSB_CONTROL3_AUTO_RETRY 1 + +#define N_RETRIES_MASK (0x03 << 1) +#define N_RETRIES(n) ((n) << 1) +#define AUTO_RETRY (0x01 << 0) + + +/* Mask1 register */ +#define FUSB_MASK1 0x0A +#define FUSB_MASK1_M_VBUSOK (1 << 7) +#define FUSB_MASK1_M_ACTIVITY (1 << 6) +#define FUSB_MASK1_M_COMP_CHNG (1 << 5) +#define FUSB_MASK1_M_CRC_CHK (1 << 4) +#define FUSB_MASK1_M_ALERT (1 << 3) +#define FUSB_MASK1_M_WAKE (1 << 2) +#define FUSB_MASK1_M_COLLISION (1 << 1) +#define FUSB_MASK1_M_BC_LVL (1 << 0) + +/* Power register */ +#define FUSB_POWER 0x0B +#define PWR_INT_OSC (0x01 << 3) /* Enable internal oscillator */ +#define PWR_MEASURE (0x01 << 2) /* Measure block powered */ +#define PWR_RECEIVER (0x01 << 1) /* Receiver powered and current reference for Measure block */ +#define PWR_BANDGAP (0x01 << 0) /* Bandgap and wake circuitry */ + + +/* Reset register */ +#define FUSB_RESET 0x0C +#define FUSB_RESET_PD_RESET (1 << 1) +#define FUSB_RESET_SW_RES 1 + +/* OCPreg register */ +#define FUSB_OCPREG 0x0D +#define FUSB_OCPREG_OCP_RANGE (1 << 3) +#define FUSB_OCPREG_OCP_CUR_SHIFT 0 +#define FUSB_OCPREG_OCP_CUR (0x7 << FUSB_OCPREG_OCP_CUR_SHIFT) + +/* Maska register */ +#define FUSB_MASKA 0x0E +#define FUSB_MASKA_M_OCP_TEMP (1 << 7) +#define FUSB_MASKA_M_TOGDONE (1 << 6) +#define FUSB_MASKA_M_SOFTFAIL (1 << 5) +#define FUSB_MASKA_M_RETRYFAIL (1 << 4) +#define FUSB_MASKA_M_HARDSENT (1 << 3) +#define FUSB_MASKA_M_TXSENT (1 << 2) +#define FUSB_MASKA_M_SOFTRST (1 << 1) +#define FUSB_MASKA_M_HARDRST 1 + +/* Maskb register */ +#define FUSB_MASKB 0x0F +#define FUSB_MASKB_M_GCRCSENT 1 + +/* Control4 register */ +#define FUSB_CONTROL4 0x10 +#define FUSB_CONTROL4_TOG_EXIT_AUD 1 + +/* Status0a register */ +#define FUSB_STATUS0A 0x3C +#define FUSB_STATUS0A_SOFTFAIL (1 << 5) +#define FUSB_STATUS0A_RETRYFAIL (1 << 4) +#define FUSB_STATUS0A_POWER3 (1 << 3) +#define FUSB_STATUS0A_POWER2 (1 << 2) +#define FUSB_STATUS0A_SOFTRST (1 << 1) +#define FUSB_STATUS0A_HARDRST 1 + +/* Status1a register */ +#define FUSB_STATUS1A 0x3D +#define FUSB_STATUS1A_TOGSS_SHIFT 3 +#define FUSB_STATUS1A_TOGSS (0x7 << FUSB_STATUS1A_TOGSS_SHIFT) +#define FUSB_STATUS1A_RXSOP2DB (1 << 2) +#define FUSB_STATUS1A_RXSOP1DB (1 << 1) +#define FUSB_STATUS1A_RXSOP 1 + +/* Interrupta register */ +#define FUSB_INTERRUPTA 0x3E +#define FUSB_INTERRUPTA_I_OCP_TEMP (1 << 7) +#define FUSB_INTERRUPTA_I_TOGDONE (1 << 6) +#define FUSB_INTERRUPTA_I_SOFTFAIL (1 << 5) +#define FUSB_INTERRUPTA_I_RETRYFAIL (1 << 4) +#define FUSB_INTERRUPTA_I_HARDSENT (1 << 3) +#define FUSB_INTERRUPTA_I_TXSENT (1 << 2) +#define FUSB_INTERRUPTA_I_SOFTRST (1 << 1) +#define FUSB_INTERRUPTA_I_HARDRST 1 + +/* Interruptb register */ +#define FUSB_INTERRUPTB 0x3F +#define FUSB_INTERRUPTB_I_GCRCSENT 1 + +/* Status0 register */ +#define FUSB_STATUS0 0x40 +#define FUSB_STATUS0_VBUSOK (1 << 7) +#define FUSB_STATUS0_ACTIVITY (1 << 6) +#define FUSB_STATUS0_COMP (1 << 5) +#define FUSB_STATUS0_CRC_CHK (1 << 4) +#define FUSB_STATUS0_ALERT (1 << 3) +#define FUSB_STATUS0_WAKE (1 << 2) +#define FUSB_STATUS0_BC_LVL_SHIFT 0 +#define FUSB_STATUS0_BC_LVL (0x3 << FUSB_STATUS0_BC_LVL_SHIFT) + +/* Status1 register */ +#define FUSB_STATUS1 0x41 +#define FUSB_STATUS1_RXSOP2 (1 << 7) +#define FUSB_STATUS1_RXSOP1 (1 << 6) +#define FUSB_STATUS1_RX_EMPTY (1 << 5) +#define FUSB_STATUS1_RX_FULL (1 << 4) +#define FUSB_STATUS1_TX_EMPTY (1 << 3) +#define FUSB_STATUS1_TX_FULL (1 << 2) +#define FUSB_STATUS1_OVRTEMP (1 << 1) +#define FUSB_STATUS1_OCP 1 + +/* Interrupt register */ +#define FUSB_INTERRUPT 0x42 +#define FUSB_INTERRUPT_I_VBUSOK (1 << 7) +#define FUSB_INTERRUPT_I_ACTIVITY (1 << 6) +#define FUSB_INTERRUPT_I_COMP_CHNG (1 << 5) +#define FUSB_INTERRUPT_I_CRC_CHK (1 << 4) +#define FUSB_INTERRUPT_I_ALERT (1 << 3) +#define FUSB_INTERRUPT_I_WAKE (1 << 2) +#define FUSB_INTERRUPT_I_COLLISION (1 << 1) +#define FUSB_INTERRUPT_I_BC_LVL 1 + +/* FIFOs register */ +#define FUSB_FIFOS 0x43 + +#define FUSB_FIFO_TX_TXON 0xA1 +#define FUSB_FIFO_TX_SOP1 0x12 +#define FUSB_FIFO_TX_SOP2 0x13 +#define FUSB_FIFO_TX_SOP3 0x1B +#define FUSB_FIFO_TX_RESET1 0x15 +#define FUSB_FIFO_TX_RESET2 0x16 +#define FUSB_FIFO_TX_PACKSYM 0x80 +#define FUSB_FIFO_TX_JAM_CRC 0xFF +#define FUSB_FIFO_TX_EOP 0x14 +#define FUSB_FIFO_TX_TXOFF 0xFE + +#define FUSB_FIFO_RX_TOKEN_BITS 0xE0 +#define FUSB_FIFO_RX_SOP 0xE0 +#define FUSB_FIFO_RX_SOP1 0xC0 +#define FUSB_FIFO_RX_SOP2 0xA0 +#define FUSB_FIFO_RX_SOP1DB 0x80 +#define FUSB_FIFO_RX_SOP2DB 0x60 diff --git a/esphome/components/fusb302b/fusb302b.cpp b/esphome/components/fusb302b/fusb302b.cpp new file mode 100644 index 00000000..8512db76 --- /dev/null +++ b/esphome/components/fusb302b/fusb302b.cpp @@ -0,0 +1,516 @@ +#include "fusb302b.h" + +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#include "esp_rom_gpio.h" +#include +#include + +#include "fusb302_defines.h" + +namespace esphome { +namespace power_delivery { + +static const char *const TAG = "fusb302b"; + +enum FUSB302_transmit_data_tokens_t { + TX_TOKEN_TXON = 0xA1, + TX_TOKEN_SOP1 = 0x12, + TX_TOKEN_SOP2 = 0x13, + TX_TOKEN_SOP3 = 0x1B, + TX_TOKEN_RESET1 = 0x15, + TX_TOKEN_RESET2 = 0x16, + TX_TOKEN_PACKSYM = 0x80, + TX_TOKEN_JAM_CRC = 0xFF, + TX_TOKEN_EOP = 0x14, + TX_TOKEN_TXOFF = 0xFE, +}; + +TaskHandle_t xReaderTaskHandle = NULL; +TaskHandle_t xProcessTaskHandle = NULL; +QueueHandle_t pd_message_queue = NULL; + + +// ISR handler +void IRAM_ATTR fusb302b_isr_handler(void *arg) { + BaseType_t xHigherPriorityTaskWoken = pdFALSE; + + // Notify the task from the ISR, using xTaskNotifyFromISR + xTaskNotifyFromISR(xReaderTaskHandle, 0x01, eSetBits, &xHigherPriorityTaskWoken); + + // Perform a context switch if needed + if (xHigherPriorityTaskWoken == pdTRUE) { + portYIELD_FROM_ISR(); + } +} + + +static void msg_reader_task(void *params){ + FUSB302B* fusb302b = (FUSB302B*) params; + uint32_t ulNotificationValue; + + fusb_status regs; + + PDEventInfo event_info; + PDMsg &msg = event_info.msg; + + printf("MSG READER TASK STARTED \n"); + fusb302b->enable_auto_crc(); + fusb302b->fusb_reset_(); + + while(true){ + xTaskNotifyWait(0x00, 0xFFFFFFFF, &ulNotificationValue, portMAX_DELAY); //500 / portTICK_PERIOD_MS portMAX_DELAY + fusb302b->read_status(regs); + if( regs.interruptb & FUSB_INTERRUPTB_I_GCRCSENT ){ + event_info.event = PD_EVENT_RECEIVED_MSG; + while( !(regs.status1 & FUSB_STATUS1_RX_EMPTY) ){ + if( fusb302b->read_message_(msg) ){ + xQueueSend(pd_message_queue, &event_info, 0); + } else { + printf("Reading failed\n"); + } + fusb302b->read_status_register(FUSB_STATUS1, regs.status1); + } + } + + if ( regs.interrupta & FUSB_INTERRUPTA_I_HARDRST){ + event_info.event = PD_EVENT_HARD_RESET; + printf(">>>FUSB_STATUS0A_HARDRST<<<\n"); + } else if ( regs.interrupta & FUSB_INTERRUPTA_I_SOFTRST) + { + printf(">>>SOFT_RESET_REQUEST<<<\n"); + event_info.event = PD_EVENT_SOFT_RESET; + } else if ( regs.interrupta & FUSB_INTERRUPTA_I_RETRYFAIL) + { + event_info.event = PD_EVENT_SENDING_MSG_FAILED; + printf("Message did not get acknowledged.\n"); + } + } + fusb302b->disable_auto_crc(); + +} + + +static void trigger_task(void *params){ + FUSB302B* fusb302b = (FUSB302B*) params; + uint32_t ulNotificationValue; + + pd_message_queue = xQueueCreate( 5, sizeof(PDEventInfo)); + + // Create the task that will wait for notifications + xTaskCreatePinnedToCore( msg_reader_task, "fusb3202b_read_task", 4096, fusb302b, configMAX_PRIORITIES/2, &xReaderTaskHandle, 1 ); + PDEventInfo event_info; + + while(true){ + if( xQueueReceive(pd_message_queue, &event_info, portMAX_DELAY) == pdTRUE ){ + //delay needed for getting fusb302b ready for receiving i2c again, is this the right place though? + vTaskDelay(pdMS_TO_TICKS(1)); + void taskENTER_CRITICAL( void ); + PDMsg &msg = event_info.msg; + //printf( "PD-Received new message with id: %d (%d, %d) [%u].\n", msg.id, msg.type, msg.num_of_obj, millis()); + fusb302b->handle_message_(msg); + void taskEXIT_CRITICAL( void ); + } + } +} + + + +void FUSB302B::setup(){ + this->i2c_lock_ = xSemaphoreCreateBinary(); + if (this->i2c_lock_ == NULL) { + ESP_LOGD(TAG, "Failed to create semaphore."); + this->mark_failed(); + return; + } + + // Release the semaphore initially + xSemaphoreGive(this->i2c_lock_); + + if( this->check_chip_id() ){ + ESP_LOGD(TAG, "FUSB302 found, initializing..."); + } else { + ESP_LOGD(TAG, "FUSB302 not found."); + this->mark_failed(); + return; + } + + if( !init_fusb_settings_() ){ + ESP_LOGE(TAG, "Couldn't setup FUSB302."); + this->mark_failed(); + return; + } + this->startup_delay_ = millis(); +} + + +void FUSB302B::dump_config(){ +} + +void FUSB302B::loop(){ + this->check_status_(); + if( this->contract_timer_ && millis() - this->contract_timer_ > 1000 ){ + this->publish_(); + this->contract_timer_ = 0; + } +} + + +bool FUSB302B::cc_line_selection_(){ + /* Measure CC1 */ + this->reg(FUSB_SWITCHES0) = FUSB_SWITCHES0_PDWN_1 | FUSB_SWITCHES0_PDWN_2 | FUSB_SWITCHES0_MEAS_CC1; + this->reg(FUSB_SWITCHES1) = 0x01 << FUSB_SWITCHES1_SPECREV_SHIFT; + this->reg(FUSB_MEASURE) = 49; + delay(5); + + uint8_t cc1 = this->reg(FUSB_STATUS0).get() & FUSB_STATUS0_BC_LVL; + for (uint8_t i = 0; i < 5; i++) { + uint8_t tmp = this->reg(FUSB_STATUS0).get() & FUSB_STATUS0_BC_LVL; + if (cc1 != tmp) { + return false; + } + } + + /* Measure CC2 */ + this->reg(FUSB_SWITCHES0) = FUSB_SWITCHES0_PDWN_1 | FUSB_SWITCHES0_PDWN_2 | FUSB_SWITCHES0_MEAS_CC2; + delay(5); + uint8_t cc2 = this->reg(FUSB_STATUS0).get() & FUSB_STATUS0_BC_LVL; + for (uint8_t i = 0; i < 5; i++) { + uint8_t tmp = this->reg(FUSB_STATUS0).get() & FUSB_STATUS0_BC_LVL; + if (cc2 != tmp) { + return false; + } + } + + /* Select the correct CC line for BMC signaling; also enable AUTO_CRC */ + if (cc1 > 0 && cc2 == 0 ) { + ESP_LOGD(TAG, "CC select: 1"); + + // PWDN1 | PWDN2 | MEAS_CC1 + this->reg(FUSB_SWITCHES0) = 0x07; + + this->reg(FUSB_SWITCHES1) = ( + FUSB_SWITCHES1_TXCC1 | + //FUSB_SWITCHES1_AUTO_CRC | + (0x01 << FUSB_SWITCHES1_SPECREV_SHIFT)); + + } else if (cc1 == 0 && cc2 > 0) { + ESP_LOGD(TAG, "CC select: 2"); + // PWDN1 | PWDN2 | MEAS_CC2 + this->reg(FUSB_SWITCHES0) = 0x0B; + + this->reg(FUSB_SWITCHES1) = ( + FUSB_SWITCHES1_TXCC2 | + //FUSB_SWITCHES1_AUTO_CRC | + (0x01 << FUSB_SWITCHES1_SPECREV_SHIFT)); + } + else{ + return false; + } + + return true; +} + + +void FUSB302B::fusb_reset_(){ + if (xSemaphoreTake( this->i2c_lock_, pdMS_TO_TICKS(100)) != pdTRUE ) { + return; + } + /* Flush the TX buffer */ + this->reg(FUSB_CONTROL0) = FUSB_CONTROL0_TX_FLUSH; + + /* Flush the RX buffer */ + this->reg(FUSB_CONTROL1) = FUSB_CONTROL1_RX_FLUSH; + + /* Reset the PD logic */ + this->reg(FUSB_RESET) = FUSB_RESET_PD_RESET; + + xSemaphoreGive(this->i2c_lock_); + this->last_received_msg_id_ = 255; + PDMsg::msg_cnter_ = 0; + return; +} + + +bool FUSB302B::read_status( fusb_status &status ){ + if (xSemaphoreTake( this->i2c_lock_, pdMS_TO_TICKS(100)) != pdTRUE ) { + return false; + } + int err = this->read_register(FUSB_STATUS0A, status.bytes , 7); + xSemaphoreGive(this->i2c_lock_); + return err == 0; +} + + + +void FUSB302B::check_status_(){ + static uint32_t last_check = millis(); + if( millis() - last_check < 1000 ){ + return; + } + last_check = millis(); + + + switch( this->state_){ + case FUSB302_STATE_UNATTACHED: + { + if( this->startup_delay_ && millis() - this->startup_delay_ < 2000) { + return; + } + + if (xSemaphoreTake( this->i2c_lock_, pdMS_TO_TICKS(100)) != pdTRUE ) { + return; + } + /* enable internal oscillator */ + this->reg(FUSB_POWER) = PWR_BANDGAP | PWR_RECEIVER | PWR_MEASURE | PWR_INT_OSC; + + bool connected = this->cc_line_selection_(); + xSemaphoreGive(this->i2c_lock_); + + if( !connected ){ + return; + } + + if( this->startup_delay_ ) + { + ESP_LOGD(TAG, "Statup delay reached!"); + this->startup_delay_ = 0; + + gpio_num_t irq_gpio_pin = static_cast(this->irq_pin_); + + gpio_config_t io_conf; + io_conf.pin_bit_mask = (1ULL << irq_gpio_pin); + io_conf.mode = GPIO_MODE_INPUT; + io_conf.pull_up_en = GPIO_PULLUP_ENABLE; + io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE; + io_conf.intr_type = GPIO_INTR_NEGEDGE ; + + gpio_config(&io_conf); + + // Install ISR service and attach the ISR handler + gpio_set_intr_type((gpio_num_t) irq_gpio_pin , GPIO_INTR_NEGEDGE ); + gpio_install_isr_service(0); + gpio_isr_handler_add( irq_gpio_pin, fusb302b_isr_handler, NULL); + + // Create the task that will wait for notifications + xTaskCreatePinnedToCore(trigger_task, "fusb3202b_task", 4096, this , configMAX_PRIORITIES, &xProcessTaskHandle, 0); + delay(1); + } else { + this->enable_auto_crc(); + this->fusb_reset_(); + } + + this->get_src_cap_time_stamp_ = millis(); + this->get_src_cap_retry_count_ = 0; + this->wait_src_cap_ = true; + + this->state_ = FUSB302_STATE_ATTACHED; + this->set_state_(PD_STATE_DEFAULT_CONTRACT); + ESP_LOGD(TAG, "USB-C attached"); + break; + } + case FUSB302_STATE_ATTACHED: + + if( this->check_ams() ){ + return; + } + + if( this->wait_src_cap_ ){ + if( get_src_cap_retry_count_ && millis() - get_src_cap_time_stamp_ < 5000 ){ + return; + } + if( !get_src_cap_retry_count_ ){ + get_src_cap_retry_count_++; + get_src_cap_time_stamp_ = millis(); + return; + } + get_src_cap_retry_count_++; + get_src_cap_time_stamp_ = millis(); + if( get_src_cap_retry_count_ < 4){ + /* clear interrupts */ + this->read_status(); + this->send_message_(PDMsg( pd_control_msg_type::PD_CNTRL_GET_SOURCE_CAP)); + } else { + ESP_LOGD(TAG, "send get_source_cap reached max count."); + if( !this->tried_soft_reset_ ){ + this->fusb_reset_(); + this->send_message_(PDMsg( pd_control_msg_type::PD_CNTRL_SOFT_RESET)); + this->get_src_cap_retry_count_ = 2; + this->tried_soft_reset_ = true; + } else { + ESP_LOGD(TAG, "PD-Negotiaton failed. Staying with default 5V supply."); + this->wait_src_cap_ = false; + this->active_ams_ = false; + } + } + } + break; + } +} + + +bool FUSB302B::check_chip_id(){ + if (xSemaphoreTake( this->i2c_lock_, pdMS_TO_TICKS(1000)) != pdTRUE ) { + return false; + } + uint8_t dev_id = this->reg(FUSB_DEVICE_ID).get(); + xSemaphoreGive(this->i2c_lock_); + ESP_LOGD(TAG, "reported device id: %d", dev_id ); + return (dev_id == 0x81) || (dev_id == 0x91); +} + +bool FUSB302B::enable_auto_crc(){ + if (xSemaphoreTake( this->i2c_lock_, pdMS_TO_TICKS(100)) != pdTRUE ) { + return false; + } + uint8_t sw1 = this->reg(FUSB_SWITCHES1).get(); + this->reg(FUSB_SWITCHES1) = sw1 | FUSB_SWITCHES1_AUTO_CRC; + xSemaphoreGive(this->i2c_lock_); + return true; +} + +bool FUSB302B::disable_auto_crc(){ + if (xSemaphoreTake( this->i2c_lock_, pdMS_TO_TICKS(100)) != pdTRUE ) { + return false; + } + uint8_t sw1 = this->reg(FUSB_SWITCHES1).get(); + this->reg(FUSB_SWITCHES1) = sw1; + xSemaphoreGive(this->i2c_lock_); + return true; +} + +bool FUSB302B::read_status_register(uint8_t reg, uint8_t &value){ + if (xSemaphoreTake( this->i2c_lock_, pdMS_TO_TICKS(100)) != pdTRUE ) { + return false; + } + int err = this->read_register(reg, &value, 1); + xSemaphoreGive(this->i2c_lock_); + return err == 0; +} + + +bool FUSB302B::init_fusb_settings_(){ + if (xSemaphoreTake( this->i2c_lock_, pdMS_TO_TICKS(100)) != pdTRUE ) { + return false; + } + + // set all registers back to default + this->reg(FUSB_RESET) = FUSB_RESET_SW_RES; + this->fusb_reset_(); + + /* Set interrupt masks */ + //this->reg(FUSB_MASK1) = 0xFF; //~FUSB_MASK1_M_CRC_CHK; + this->reg(FUSB_MASK1) = 0x51; + this->reg(FUSB_MASKA) = ~( + FUSB_MASKA_M_RETRYFAIL | + FUSB_MASKA_M_TXSENT | + FUSB_MASKA_M_SOFTRST | + FUSB_MASKA_M_HARDRST + ); + this->reg(FUSB_MASKA) = 0 ; + //Mask the I_GCRCSENT interrupt + this->reg(FUSB_MASKB) = 0;//FUSB_MASKB_M_GCRCSENT; + + /* disable global interrupt masking*/ + uint8_t cntrl0 = this->reg(FUSB_CONTROL0).get(); + this->reg(FUSB_CONTROL0) = cntrl0 & ~FUSB_CONTROL0_INT_MASK; + + /* Enable automatic retransmission */ + uint8_t cntrl3 = this->reg(FUSB_CONTROL3).get(); + cntrl3 &= ~FUSB_CONTROL3_N_RETRIES_MASK; + cntrl3 |= (0x03 << FUSB_CONTROL3_N_RETRIES_SHIFT) | FUSB_CONTROL3_AUTO_RETRY; + this->reg(FUSB_CONTROL3) = cntrl3; + + + this->reg(FUSB_POWER) = 0x0F; + this->fusb_reset_(); + xSemaphoreGive(this->i2c_lock_); + return true; +} + + +bool FUSB302B::read_message_(PDMsg &msg){ + + if (xSemaphoreTake( this->i2c_lock_, pdMS_TO_TICKS(100)) != pdTRUE ) { + return false; + } + + uint8_t fifo_byte = this->reg(FUSB_FIFOS).get(); + uint8_t ret = 0; + + if( ( fifo_byte & FUSB_FIFO_RX_TOKEN_BITS) != FUSB_FIFO_RX_SOP ) + { + ret = 1; + } + + uint16_t header; + ret |= this->read_register(FUSB_FIFOS, (uint8_t*) &header, 2); + msg.set_header( header ); + + if (msg.num_of_obj > 7 ){ + xSemaphoreGive(this->i2c_lock_); + return false; + } else if ( msg.num_of_obj > 0){ + ret |= this->read_register(FUSB_FIFOS, (uint8_t*) msg.data_objects, msg.num_of_obj * sizeof(uint32_t) ); + } + + /* Read CRC32 only, the PHY already checked it. */ + uint8_t dummy[4]; + ret |= this->read_register(FUSB_FIFOS, dummy, 4); + + xSemaphoreGive(this->i2c_lock_); + return (ret == 0); +} + +bool FUSB302B::send_message_(const PDMsg &msg){ + if (xSemaphoreTake( this->i2c_lock_, pdMS_TO_TICKS(100)) != pdTRUE ) { + return false; + } + + uint8_t buf[40]; + uint8_t *pbuf = buf; + + uint16_t header = msg.get_coded_header(); + uint8_t obj_count = msg.num_of_obj; + + *pbuf++ = (uint8_t)TX_TOKEN_SOP1; + *pbuf++ = (uint8_t)TX_TOKEN_SOP1; + *pbuf++ = (uint8_t)TX_TOKEN_SOP1; + *pbuf++ = (uint8_t)TX_TOKEN_SOP2; + *pbuf++ = (uint8_t)TX_TOKEN_PACKSYM | ((obj_count << 2) + 2); + *pbuf++ = header & 0xFF; header >>= 8; + *pbuf++ = header & 0xFF; + for (uint8_t i = 0; i < obj_count; i++) { + uint32_t d = msg.data_objects[i]; + *pbuf++ = d & 0xFF; d >>= 8; + *pbuf++ = d & 0xFF; d >>= 8; + *pbuf++ = d & 0xFF; d >>= 8; + *pbuf++ = d & 0xFF; + } + *pbuf++ = (uint8_t)TX_TOKEN_JAM_CRC; + *pbuf++ = (uint8_t)TX_TOKEN_EOP; + *pbuf++ = (uint8_t)TX_TOKEN_TXOFF; + *pbuf++ = (uint8_t)TX_TOKEN_TXON; + + + int err = this->write_register( FUSB_FIFOS, buf, pbuf - buf); + if( err != i2c::ERROR_OK ){ + printf("Sending Message (%d) failed err: %d.\n", (int) msg.type, err ); + } + // else { + // printf("Sent Message (%d) id: %d. [%d] \n", (int) msg.type, msg.id, millis() ); + // } + + //msg.debug_log(); + xSemaphoreGive(this->i2c_lock_); + return true; +} + + + + + +} +} \ No newline at end of file diff --git a/esphome/components/fusb302b/fusb302b.h b/esphome/components/fusb302b/fusb302b.h new file mode 100644 index 00000000..c2897835 --- /dev/null +++ b/esphome/components/fusb302b/fusb302b.h @@ -0,0 +1,85 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/core/hal.h" + +#include "pd.h" + +namespace esphome { +namespace power_delivery { + + +enum FUSB302_state_t { + FUSB302_STATE_UNATTACHED = 0, + FUSB302_STATE_ATTACHED +}; + +typedef union { + uint8_t bytes[7]; + struct { + uint8_t status0a; + uint8_t status1a; + uint8_t interrupta; + uint8_t interruptb; + uint8_t status0; + uint8_t status1; + uint8_t interrupt; + }; + } fusb_status; + + + +class FUSB302B : public PowerDelivery, public Component, protected i2c::I2CDevice { +public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::HARDWARE; } + void loop() override; + + bool send_message_(const PDMsg &msg) override; + bool read_message_(PDMsg &msg) override; + bool read_status(){ fusb_status regs; return read_status(regs); } + bool read_status(fusb_status &status); + + bool read_status_register( uint8_t register, uint8_t &value); + + + void set_irq_pin(int irq_pin){this->irq_pin_ = irq_pin;} + void set_i2c_address(uint8_t address) { i2c::I2CDevice::set_i2c_address(address); } + void set_i2c_bus( i2c::I2CBus *bus) { i2c::I2CDevice::set_i2c_bus(bus); } + + bool check_chip_id(); + bool enable_auto_crc(); + bool disable_auto_crc(); + +public: + bool cc_line_selection_(); + void fusb_reset_(); + + void check_status_(); + + + FUSB302_state_t state_{FUSB302_STATE_UNATTACHED}; + + uint32_t response_timer_{0}; + uint32_t startup_delay_{0}; + + + + +protected: + void publish_() override { + this->defer([this]() { this->state_callback_.call(); }); + } + + bool init_fusb_settings_(); + + SemaphoreHandle_t i2c_lock_; + + int irq_pin_{0}; +}; + +} +} \ No newline at end of file diff --git a/esphome/components/fusb302b/pd.cpp b/esphome/components/fusb302b/pd.cpp new file mode 100644 index 00000000..0cf45712 --- /dev/null +++ b/esphome/components/fusb302b/pd.cpp @@ -0,0 +1,247 @@ +#include "pd.h" + +#include +#include +#include +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace power_delivery { + +static const char *const TAG = "PowerDelivery"; + + +pd_contract_t pd_parse_power_info( const pd_pdo_t &pdo ) +{ + pd_contract_t power_info; + power_info.type = static_cast(pdo >> 30); + switch (power_info.type) { + case PD_PDO_TYPE_FIXED_SUPPLY: + /* Reference: 6.4.1.2.3 Source Fixed Supply Power Data Object */ + power_info.min_v = 0; + power_info.max_v = (pdo >> 10) & 0x3FF; /* B19...10 Voltage in 50mV units */ + power_info.max_i = (pdo >> 0) & 0x3FF; /* B9 ...0 Max Current in 10mA units */ + power_info.max_p = 0; + break; + case PD_PDO_TYPE_BATTERY: + /* Reference: 6.4.1.2.5 Battery Supply Power Data Object */ + power_info.min_v = (pdo >> 10) & 0x3FF; /* B19...10 Min Voltage in 50mV units */ + power_info.max_v = (pdo >> 20) & 0x3FF; /* B29...20 Max Voltage in 50mV units */ + power_info.max_i = 0; + power_info.max_p = (pdo >> 0) & 0x3FF; /* B9 ...0 Max Allowable Power in 250mW units */ + break; + case PD_PDO_TYPE_VARIABLE_SUPPLY: + /* Reference: 6.4.1.2.4 Variable Supply (non-Battery) Power Data Object */ + power_info.min_v = (pdo >> 10) & 0x3FF; /* B19...10 Min Voltage in 50mV units */ + power_info.max_v = (pdo >> 20) & 0x3FF; /* B29...20 Max Voltage in 50mV units */ + power_info.max_i = (pdo >> 0) & 0x3FF; /* B9 ...0 Max Current in 10mA units */ + power_info.max_p = 0; + break; + case PD_PDO_TYPE_AUGMENTED_PDO: + /* Reference: 6.4.1.3.4 Programmable Power Supply Augmented Power Data Object */ + power_info.max_v = ((pdo >> 17) & 0xFF) * 2; /* B24...17 Max Voltage in 100mV units */ + power_info.min_v = ((pdo >> 8) & 0xFF) * 2; /* B15...8 Min Voltage in 100mV units */ + power_info.max_i = ((pdo >> 0) & 0x7F) * 5; /* B6 ...0 Max Current in 50mA units */ + power_info.max_p = 0; + break; + } + return power_info; +} + + +static PDMsg build_source_cap_response( pd_contract_t pwr_info, uint8_t pos ) +{ + /* Reference: 6.4.2 Request Message */ + constexpr uint32_t templ = ( + //((uint32_t) 1 << 22) | /* B22 EPR Mode Capable */ + //((uint32_t) 1 << 23) | /* B23 Unchunked Extended Messages Supported */ + ((uint32_t) 1 << 24) | /* B24 No USB Suspend */ + ((uint32_t) 1 << 25) /* B25 USB Communication Capable */ + //((uint32_t) 1 << 26) /* B26 Capability Mismatch */ + //((uint32_t) 1 << 27) /* B27 GiveBack flag = 0 (depricated)*/ + ); + uint32_t data = templ; + if (pwr_info.type != PD_PDO_TYPE_AUGMENTED_PDO) { + uint32_t req = pwr_info.max_i ? pwr_info.max_i : pwr_info.max_p; + //uint32_t req = 10; + data |= ((uint32_t) req << 0) | /* B9 ...0 Max Operating Current 10mA units / Max Operating Power in 250mW units */ + ((uint32_t) req << 10) | /* B19...10 Operating Current 10mA units / Operating Power in 250mW units */ /* B21...20 Reserved - Shall be set to zero */ + ((uint32_t) pos << 28); /* B30...28 Object position (000b is Reserved and Shall Not be used) */ + } else { + ESP_LOGE(TAG, "Augmented PDO is not supported yet" ); + } + return PDMsg(pd_data_msg_type::PD_DATA_REQUEST, &data, 1); +} + +PDMsg PowerDelivery::create_fallback_request_message() const { + //request first PDO, which is always the 5V Fixed Supply + const uint8_t pos = 1; + //set max and operational current to 500mA (default maximum for usb) + constexpr uint32_t data[1] = { + ((uint32_t) 30 << 0) | /* B9 ...0 Max Operating Current 10mA units */ + ((uint32_t) 10 << 10) | /* B19...10 Operating Current 10mA units */ + /* B21...20 Reserved - Shall be set to zero */ + //((uint32_t) 1 << 22) | /* B22 EPR Mode Capable */ + //((uint32_t) 1 << 23) | /* B23 Unchunked Extended Messages Supported */ + ((uint32_t) 1 << 24) | /* B24 No USB Suspend */ + ((uint32_t) 1 << 25) | /* B25 USB Communication Capable */ + //((uint32_t) 1 << 26) | /* B26 Capability Mismatch */ + //((uint32_t) 1 << 27) | /* B27 GiveBack flag = 0 (depricated)*/ + ((uint32_t) 1 << 28) /* B31...28 Object position (000b is Reserved and Shall Not be used) */ + }; + return PDMsg(pd_data_msg_type::PD_DATA_REQUEST, data, 1); +} + +bool PowerDelivery::respond_to_src_cap_msg_( const PDMsg &msg ){ + // {.limit = 100, .use_voltage = 1, .use_current = 0}, /* PD_POWER_OPTION_MAX_20V */ + pd_contract_t selected_info; + memset( &selected_info, 0 , sizeof(pd_contract_t) ); + uint8_t selected = 255; + for(int idx=0; idx < msg.num_of_obj; idx++){ + pd_contract_t pwr_info = pd_parse_power_info( msg.data_objects[idx] ); + // printf("SRC_CAP: type: %d V(%d - %d) I(%d) P(%d)\n", + // pwr_info.type, + // pwr_info.min_v, + // (pwr_info.max_v * 50) / 1000, + // pwr_info.max_i, + // pwr_info.max_p + // ); + if (pwr_info.type == PD_PDO_TYPE_AUGMENTED_PDO) { + continue; + } else { + uint8_t v = true ? pwr_info.max_v >> 2 : 1; + uint8_t i = false ? pwr_info.max_i >> 2 : 1; + uint16_t power = (uint16_t)v * i; /* reduce 10-bit power info to 8-bit and use 8-bit x 8-bit multiplication */ + if ( pwr_info.max_v * 50 / 1000 <= this->request_voltage_ || selected == 255) { + selected_info = pwr_info; + selected = idx; + } + } + } + this->requested_contract_ = selected_info; + + //PDMsg response = create_fallback_request_message(); + PDMsg response = build_source_cap_response(selected_info, selected + 1); + this->send_message_( response ); + + return true; +} + +void PowerDelivery::set_ams(bool ams){ + this->active_ams_ = ams; + if( ams ){ + this->active_ams_timer_ = millis(); + } +} + +bool PowerDelivery::check_ams(){ + if( millis() - this->active_ams_timer_ > 2000 ){ + this->active_ams_ = false; + } + return this->active_ams_; +} + +std::string PowerDelivery::get_contract_string(pd_contract_t contract) const{ + std::ostringstream oss; + oss.precision(3); + oss << (contract.max_i / 100 ); + oss << "A @ "; + oss << contract.max_v * 5 / 100; + oss << "V"; + return oss.str(); +} + +void PowerDelivery::set_contract_(pd_contract_t contract){ + this->accepted_contract_ = contract; + this->contract = this->get_contract_string(contract); + this->contract_voltage = contract.max_v; + this->contract_timer_ = millis(); +} + + +bool PowerDelivery::request_voltage(int voltage){ + if(!this->active_ams_){ + this->set_request_voltage(voltage); + this->wait_src_cap_ = true; + get_src_cap_retry_count_ = 0; + return true; + } + + return false; +} + +pd_spec_revision_t PDMsg::spec_rev_ = pd_spec_revision_t::PD_SPEC_REV_2; +uint8_t PDMsg::msg_cnter_ = 0; + +PDMsg::PDMsg(uint16_t header){ + this->set_header(header); +} + +bool PDMsg::set_header(uint16_t header){ + this->type = static_cast((header >> 0) & 0x1F); /* 4...0 Message Type */ + this->spec_rev = (pd_spec_revision_t) ((header >> 6) & 0x3); /* 7...6 Specification Revision */ + this->id = (header >> 9) & 0x7; /* 11...9 MessageID */ + this->num_of_obj = (header >> 12) & 0x7; /* 14...12 Number of Data Objects */ + this->extended = (header >> 15); /* */ + return true; +} + +PDMsg::PDMsg(pd_control_msg_type cntrl_msg_type){ + this->type = cntrl_msg_type; + this->spec_rev = this->spec_rev_; + this->id = (this->msg_cnter_) % 8; + this->num_of_obj = 0; + this->extended = false; +} + +PDMsg::PDMsg(pd_control_msg_type cntrl_msg_type, uint8_t msg_id){ + this->type = cntrl_msg_type; + this->spec_rev = this->spec_rev_; + this->id = msg_id; + this->num_of_obj = 0; + this->extended = false; +} + + +PDMsg::PDMsg(pd_data_msg_type msg_type, const uint32_t* objects, uint8_t len){ + assert( len > 0 && len < PD_MAX_NUM_DATA_OBJECTS ); + this->type = msg_type; + this->spec_rev = this->spec_rev_; + this->id = (this->msg_cnter_) % 8; + this->num_of_obj = len; + this->extended = false; + memcpy( this->data_objects, objects, len * sizeof(uint32_t) ); +} + +uint16_t PDMsg::get_coded_header() const { + uint16_t h = ((uint16_t) this->type << 0 ) | + ((uint16_t) 0x00 << 5 ) | /* DataRole 0: UFP */ + ((uint16_t) this->spec_rev << 6 ) | + ((uint16_t) 0x00 << 8 ) | /* PowerRole 0: sink */ + ((uint16_t) this->id << 9 ) | + ((uint16_t) this->num_of_obj << 12 ) | + ((uint16_t) !!(this->extended) << 15 ); + return h; +} + +void PDMsg::debug_log() const{ + ESP_LOGD(TAG, "PD Message (%d)", this->type ); + ESP_LOGD(TAG, " type: %d", this->type ); + ESP_LOGD(TAG, " rev: %d", this->spec_rev ); + ESP_LOGD(TAG, " id: %d", this->id ); + ESP_LOGD(TAG, " #obj: %d", this->num_of_obj ); + ESP_LOGD(TAG, " ext: %d", !!(this->extended) ); + ESP_LOGD(TAG, " coded: %d", this->get_coded_header()); + ESP_LOGD(TAG, "Current Cnter: %d", this->msg_cnter_); +} + +void PowerDelivery::add_on_state_callback(std::function &&callback) { + this->state_callback_.add(std::move(callback)); +} + + + +} +} \ No newline at end of file diff --git a/esphome/components/fusb302b/pd.h b/esphome/components/fusb302b/pd.h new file mode 100644 index 00000000..14e458e3 --- /dev/null +++ b/esphome/components/fusb302b/pd.h @@ -0,0 +1,303 @@ +#pragma once + +#include "esphome/core/defines.h" +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" + +#define PD_MAX_NUM_DATA_OBJECTS 7 + +namespace esphome { +namespace power_delivery { + +enum pd_spec_revision_t { + PD_SPEC_REV_1 = 0, + PD_SPEC_REV_2 = 1, + PD_SPEC_REV_3 = 2 + /* 3 Reserved */ +}; + +enum pd_data_msg_type { + /* 0 Reserved */ + PD_DATA_SOURCE_CAP = 0x01, + PD_DATA_REQUEST = 0x02, + PD_DATA_BIST = 0x03, + PD_DATA_SINK_CAP = 0x04, + PD_DATA_BATTERY_STATUS = 0x05, + PD_DATA_ALERT = 0x06, + PD_DATA_GET_COUNTRY_INFO = 0x07, + PD_DATA_ENTER_USB = 0x08, + PD_DATA_EPR_REQUEST = 0x09, + PD_DATA_EPR_MODE = 0x0A, + PD_DATA_SOURCE_INFO = 0x0B, + PD_DATA_REVISION = 0x0C, + PD_DATA_VENDOR_DEF = 0x0F +}; + +enum pd_control_msg_type { + PD_CNTRL_GOODCRC = 0x01, + PD_CNTRL_GOTOMIN = 0x02, + PD_CNTRL_ACCEPT = 0x03, + PD_CNTRL_REJECT = 0x04, + PD_CNTRL_PING = 0x05, + PD_CNTRL_PS_RDY = 0x06, + PD_CNTRL_GET_SOURCE_CAP = 0x07, + PD_CNTRL_GET_SINK_CAP = 0x08, + PD_CNTRL_DR_SWAP = 0x09, + PD_CNTRL_PR_SWAP = 0x0A, + PD_CNTRL_VCONN_SWAP = 0x0B, + PD_CNTRL_WAIT = 0x0C, + PD_CNTRL_SOFT_RESET = 0x0D, + PD_CNTRL_NOT_SUPPORTED = 0x10, + PD_CNTRL_GET_SOURCE_CAP_EXTENDED = 0x11, + PD_CNTRL_GET_STATUS = 0x12, + PD_CNTRL_FR_SWAP = 0x13, + PD_CNTRL_GET_PPS_STATUS = 0x14, + PD_CNTRL_GET_COUNTRY_CODES = 0x15, + PD_CNTRL_GET_SINK_CAP_EXTENDED = 0x16, + PD_CNTRL_GET_SOURCE_INFO = 0x17, + PD_CNTRL_GET_REVISION = 0x18 +}; + +enum pd_power_data_obj_type { /* Power data object type */ + PD_PDO_TYPE_FIXED_SUPPLY = 0, + PD_PDO_TYPE_BATTERY = 1, + PD_PDO_TYPE_VARIABLE_SUPPLY = 2, + PD_PDO_TYPE_AUGMENTED_PDO = 3 /* USB PD 3.0 */ +}; + +enum PowerDeliveryState : uint8_t { + PD_STATE_DISCONNECTED, + PD_STATE_DEFAULT_CONTRACT, + PD_STATE_TRANSITION, + PD_STATE_EXPLICIT_SPR_CONTRACT, + PD_STATE_EXPLICIT_EPR_CONTRACT, + PD_STATE_ERROR +}; + +enum PowerDeliveryEvent : uint8_t { + PD_EVENT_ATTACHED, + PD_EVENT_DETACHED, + PD_EVENT_RECEIVED_MSG, + PD_EVENT_SENDING_MSG_FAILED, + PD_EVENT_SOFT_RESET, + PD_EVENT_HARD_RESET +}; + + +struct pd_contract_t{ + enum pd_power_data_obj_type type; + uint16_t min_v; /* Voltage in 50mV units */ + uint16_t max_v; /* Voltage in 50mV units */ + uint16_t max_i; /* Current in 10mA units */ + uint16_t max_p; /* Power in 250mW units */ + + bool operator==(const pd_contract_t& other) const { + return max_v == other.max_v && + max_i == other.max_i && + type == other.type; + } + bool operator!=(const pd_contract_t& other) const { + return !( *this == other ); + } +}; + + + +class PDMsg { +public: + PDMsg() = default; + PDMsg(uint16_t header); + PDMsg(pd_control_msg_type cntrl_msg_type); + PDMsg(pd_control_msg_type cntrl_msg_type, uint8_t msg_id); + PDMsg(pd_data_msg_type data_msg_type, const uint32_t* objects, uint8_t len); + + uint16_t get_coded_header() const; + bool set_header(uint16_t header); + + uint8_t type; + pd_spec_revision_t spec_rev; + uint8_t id; + uint8_t num_of_obj; + bool extended; + uint32_t data_objects[PD_MAX_NUM_DATA_OBJECTS]; + + void debug_log() const; + +//protected: + static uint8_t msg_cnter_; + static pd_spec_revision_t spec_rev_; +}; + + + + +class PDEventInfo { +public: + PowerDeliveryEvent event; + PDMsg msg; +}; + + + + +typedef uint32_t pd_pdo_t; + +class PowerDelivery { +public: + PowerDeliveryState state{PD_STATE_DISCONNECTED}; + int contract_voltage{0}; + int measured_voltage{0}; + std::string contract{"0.3A @ 5V"}; + PowerDeliveryState prev_state_{PD_STATE_DISCONNECTED}; + + bool request_voltage(int voltage); + + virtual bool send_message_(const PDMsg &msg) = 0; + virtual bool read_message_(PDMsg &msg) = 0; + + PDMsg create_fallback_request_message() const; + bool handle_message_(const PDMsg &msg); + + void set_request_voltage(int voltage){this->request_voltage_=voltage;} + std::string get_contract_string(pd_contract_t contract) const; + void add_on_state_callback(std::function &&callback); + + void set_ams(bool ams); + bool check_ams(); + +protected: + uint32_t active_ams_timer_{0}; + bool active_ams_{false}; + void protocol_reset_(); + + uint8_t last_received_msg_id_{255}; + + bool handle_data_message_(const PDMsg &msg); + bool handle_cntrl_message_(const PDMsg &msg); + + pd_contract_t parse_power_info_( pd_pdo_t &pdo ) const; + bool respond_to_src_cap_msg_( const PDMsg &msg ); + + void set_contract_(pd_contract_t contract); + pd_contract_t requested_contract_; + pd_contract_t accepted_contract_; + pd_contract_t previous_contract_; + uint32_t contract_timer_{0}; + + void set_state_(PowerDeliveryState new_state){ + this->prev_state_ = this->state; + this->state = new_state; + } + + virtual void publish_(){} + + pd_spec_revision_t spec_revision_{pd_spec_revision_t::PD_SPEC_REV_2}; + + bool wait_src_cap_{true}; + bool tried_soft_reset_{false}; + int get_src_cap_retry_count_{0}; + uint32_t get_src_cap_time_stamp_; + + int request_voltage_{5}; + + CallbackManager state_callback_{}; +}; + + +inline PDMsg build_get_sink_cap_response(){ + /* Reference: 6.4.1.2.3 Sink Fixed Supply Power Data Object */ + constexpr uint32_t data = ( + ((uint32_t)500 << 0) | /* B9...0 Operational Current in 10mA units */ + ((uint32_t)100 << 10) | /* B19...10 Voltage in 50mV units */ + //((uint32_t) 1 << 25) | /* B25 Dual-Role Data */ + ((uint32_t) 1 << 26) | /* B26 USB Communications Capable */ + //((uint32_t) 1 << 27) | /* B27 Unconstrained Power support */ + //((uint32_t) 1 << 28) | /* B28 Higher Capability */ + //((uint32_t) 1 << 29) | /* B29 Dual Role Power */ + ((uint32_t) PD_PDO_TYPE_FIXED_SUPPLY << 30) /* B31...30 Fixed supply */ + ); + return PDMsg( pd_data_msg_type::PD_DATA_SINK_CAP, &data, 1 ); +} + + +inline bool PowerDelivery::handle_message_(const PDMsg &msg){ + if( msg.num_of_obj == 0){ + if( msg.type == PD_CNTRL_GOODCRC ) + { + PDMsg::msg_cnter_++; + return true; + } + return this->handle_cntrl_message_(msg); + } else { + return this->handle_data_message_(msg); + } +} + +inline bool PowerDelivery::handle_data_message_(const PDMsg &msg){ + if( msg.id == this->last_received_msg_id_ ){ + return false; + } + this->last_received_msg_id_ = msg.id; + switch( msg.type ){ + case PD_DATA_SOURCE_CAP: + this->set_ams(true); + this->wait_src_cap_ = false; +#if 0 + if( PDMsg::spec_rev_ == pd_spec_revision_t::PD_SPEC_REV_1 ){ + if( msg.spec_rev >= pd_spec_revision_t::PD_SPEC_REV_3 ){ + PDMsg::spec_rev_ = pd_spec_revision_t::PD_SPEC_REV_3; + } else { + PDMsg::spec_rev_ = pd_spec_revision_t::PD_SPEC_REV_2; + } + } + PDMsg::spec_rev_ = msg.spec_rev; +#endif + this->respond_to_src_cap_msg_(msg); + break; + case PD_DATA_ALERT: + break; + default: + break; + } + return true; +} + +inline bool PowerDelivery::handle_cntrl_message_(const PDMsg &msg){ + if( msg.id == this->last_received_msg_id_ ){ + return false; + } + this->last_received_msg_id_ = msg.id; + switch( msg.type ){ + case PD_CNTRL_GOODCRC: + break; + case PD_CNTRL_ACCEPT: + if( this->active_ams_ ){ + if( this->requested_contract_ != this->accepted_contract_ ){ + this->set_state_(PD_STATE_TRANSITION); + } + this->set_contract_(this->requested_contract_); + } + break; + case PD_CNTRL_PS_RDY: + this->set_ams(false); + this->set_state_(PD_STATE_EXPLICIT_SPR_CONTRACT); + break; + case PD_CNTRL_SOFT_RESET: + this->send_message_(PDMsg(pd_control_msg_type::PD_CNTRL_ACCEPT, 0)); + this->set_state_(PD_STATE_DEFAULT_CONTRACT); + PDMsg::msg_cnter_ = 0; + break; + case PD_CNTRL_GET_SINK_CAP: + this->send_message_(build_get_sink_cap_response()); + break; + default: + this->send_message_(PDMsg(pd_control_msg_type::PD_CNTRL_NOT_SUPPORTED)); + break; + break; + } + return true; +} + + +} +} \ No newline at end of file diff --git a/esphome/components/i2s_audio/__init__.py b/esphome/components/i2s_audio/__init__.py new file mode 100644 index 00000000..06c557d0 --- /dev/null +++ b/esphome/components/i2s_audio/__init__.py @@ -0,0 +1,177 @@ +from collections import defaultdict + +import esphome.config_validation as cv +import esphome.final_validate as fv +import esphome.codegen as cg + +from esphome import pins +from esphome.components import i2c +from esphome.const import CONF_ENABLE_PIN, CONF_ID, CONF_MODE, CONF_MODEL, CONF_VOLUME +from esphome.components.esp32 import get_esp32_variant +from esphome.components.esp32.const import ( + VARIANT_ESP32, + VARIANT_ESP32S2, + VARIANT_ESP32S3, + VARIANT_ESP32C3, +) + +from . import i2s_settings as i2s + +CODEOWNERS = ["@jesserockz", "@gnumpi"] +DEPENDENCIES = ["esp32"] +MULTI_CONF = True + +CONF_I2S_DOUT_PIN = "i2s_dout_pin" +CONF_I2S_DIN_PIN = "i2s_din_pin" +CONF_I2S_MCLK_PIN = "i2s_mclk_pin" +CONF_I2S_BCLK_PIN = "i2s_bclk_pin" +CONF_I2S_LRCLK_PIN = "i2s_lrclk_pin" + +CONF_I2S_AUDIO = "i2s_audio" +CONF_I2S_AUDIO_ID = "i2s_audio_id" +CONF_I2S_ACCESS_MODE = "access_mode" + +CONF_SAMPLE_RATE = "sample_rate" +CONF_BITS_PER_SAMPLE = "bits_per_sample" +CONF_PDM = "pdm" + +i2s_audio_ns = cg.esphome_ns.namespace("i2s_audio") +I2SAudioComponent = i2s_audio_ns.class_("I2SAudioComponent", cg.Component) + +# https://github.com/espressif/esp-idf/blob/master/components/soc/{variant}/include/soc/soc_caps.h +I2S_PORTS = { + VARIANT_ESP32: 2, + VARIANT_ESP32S2: 1, + VARIANT_ESP32S3: 2, + VARIANT_ESP32C3: 1, +} + +I2SAccessMode = i2s_audio_ns.enum("I2SAccessMode", is_class=True) +ACCESS_MODES = {"exclusive": I2SAccessMode.EXCLUSIVE, "duplex": I2SAccessMode.DUPLEX} + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(I2SAudioComponent), + cv.Required(CONF_I2S_LRCLK_PIN): pins.internal_gpio_output_pin_number, + cv.Optional(CONF_I2S_BCLK_PIN): pins.internal_gpio_output_pin_number, + cv.Optional(CONF_I2S_MCLK_PIN): pins.internal_gpio_output_pin_number, + cv.Optional(CONF_I2S_ACCESS_MODE, default="exclusive"): cv.enum(ACCESS_MODES), + } +) + + +def _final_validate(_): + i2s_audio_configs = fv.full_config.get()[CONF_I2S_AUDIO] + variant = get_esp32_variant() + if variant not in I2S_PORTS: + raise cv.Invalid(f"Unsupported variant {variant}") + if len(i2s_audio_configs) > I2S_PORTS[variant]: + raise cv.Invalid( + f"Only {I2S_PORTS[variant]} I2S audio ports are supported on {variant}" + ) + + +FINAL_VALIDATE_SCHEMA = _final_validate + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + cg.add(var.set_access_mode(config[CONF_I2S_ACCESS_MODE])) + cg.add(var.set_lrclk_pin(config[CONF_I2S_LRCLK_PIN])) + if CONF_I2S_BCLK_PIN in config: + cg.add(var.set_bclk_pin(config[CONF_I2S_BCLK_PIN])) + if CONF_I2S_MCLK_PIN in config: + cg.add(var.set_mclk_pin(config[CONF_I2S_MCLK_PIN])) + + +I2SReader = i2s_audio_ns.class_("I2SReader", cg.Parented.template(I2SAudioComponent)) +I2SWriter = i2s_audio_ns.class_("I2SWriter", cg.Parented.template(I2SAudioComponent)) + + +I2S_AUDIO_IN = "audio_in" +I2S_AUDIO_OUT = "audio_out" + + +CONFIG_SCHEMA_I2S_WRITER = i2s.CONFIG_SCHEMA_I2S_COMMON.extend( + { + cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent), + cv.Required(CONF_I2S_DOUT_PIN): pins.internal_gpio_output_pin_number, + } +).extend(cv.COMPONENT_SCHEMA) + + + +CONFIG_SCHEMA_I2S_READER = i2s.CONFIG_SCHEMA_I2S_COMMON.extend( + { + cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent), + cv.Required(CONF_I2S_DIN_PIN): pins.internal_gpio_input_pin_number, + cv.Required(CONF_PDM): cv.boolean, + } +) + + +def final_validate_device_schema(name: str) -> cv.Schema: + fv_data_schema = { + I2S_AUDIO_IN: None, + I2S_AUDIO_OUT: None, + CONF_SAMPLE_RATE: None, + CONF_BITS_PER_SAMPLE: None, + CONF_PDM: None, + } + + def parent_validator(config): + direction = config.get("type") + + def validator(value): + fconf = fv.full_config.get() + if CONF_I2S_AUDIO not in fconf.data: + fconf.data[CONF_I2S_AUDIO] = defaultdict(lambda: fv_data_schema) + i2s_data = fconf.data[CONF_I2S_AUDIO][value] + if not i2s_data[direction] is None: + raise cv.Invalid( + f"Multiple I2S devices [{i2s_data[direction]},{name}] registered as {direction} for {value}." + ) + i2s_data[direction] = name + + return validator + + def create_schema(config): + return cv.Schema( + {cv.Required(CONF_I2S_AUDIO_ID): parent_validator(config)}, + extra=cv.ALLOW_EXTRA, + )(config) + + return create_schema + + +async def apply_i2s_settings(var, config) -> None: + cg.add(var.set_clk_mode(config[i2s.CONF_CLK_MODE])) + cg.add(var.set_channel(config[i2s.CONF_CHANNEL])) + cg.add(var.set_sample_rate(config[i2s.CONF_SAMPLE_RATE])) + cg.add(var.set_bits_per_sample(config[i2s.CONF_BITS_PER_SAMPLE])) + cg.add(var.set_use_apll(config[i2s.CONF_USE_APLL])) + cg.add(var.set_fixed_settings(config[i2s.CONF_FIXED_SETTINGS])) + + +async def register_i2s_writer(writer, config: dict) -> None: + i2s_cntrl = await cg.get_variable(config[CONF_I2S_AUDIO_ID]) + await cg.register_parented(writer, config[CONF_I2S_AUDIO_ID]) + cg.add(i2s_cntrl.set_audio_out(writer)) + await apply_i2s_settings(writer, config) + + if CONF_I2S_DOUT_PIN in config: + cg.add(writer.set_dout_pin(config[CONF_I2S_DOUT_PIN])) + + + +async def register_i2s_reader(reader, config: dict) -> None: + i2s_cntrl = await cg.get_variable(config[CONF_I2S_AUDIO_ID]) + await cg.register_parented(reader, config[CONF_I2S_AUDIO_ID]) + cg.add(i2s_cntrl.set_audio_in(reader)) + + await apply_i2s_settings(reader, config) + cg.add(reader.set_pdm(config[CONF_PDM])) + + if CONF_I2S_DIN_PIN in config: + cg.add(reader.set_din_pin(config[CONF_I2S_DIN_PIN])) diff --git a/esphome/components/i2s_audio/i2s_audio.cpp b/esphome/components/i2s_audio/i2s_audio.cpp new file mode 100644 index 00000000..6a84b719 --- /dev/null +++ b/esphome/components/i2s_audio/i2s_audio.cpp @@ -0,0 +1,192 @@ +#include "i2s_audio.h" + +#ifdef USE_ESP32 + +#include "esphome/core/log.h" + +namespace esphome { +namespace i2s_audio { + +static const char *const TAG = "i2s_audio"; + +void I2SAudioComponent::setup() { + static i2s_port_t next_port_num = I2S_NUM_0; + + if (next_port_num >= I2S_NUM_MAX) { + ESP_LOGE(TAG, "Too many I2S Audio components!"); + this->mark_failed(); + return; + } + + this->port_ = next_port_num; + next_port_num = (i2s_port_t) (next_port_num + 1); + + ESP_LOGCONFIG(TAG, "Setting up I2S Audio..."); +} + +void I2SAudioComponent::dump_config(){ + esph_log_config(TAG, "I2SController:"); + esph_log_config(TAG, " AccessMode: %s", this->access_mode_ == I2SAccessMode::DUPLEX ? "duplex" : "exclusive" ); + esph_log_config(TAG, " Port: %d", this->get_port() ); + if( this->audio_in_ != nullptr ){ + esph_log_config(TAG, " Reader registered."); + } + if( this->audio_out_ != nullptr ){ + esph_log_config(TAG, " Writer registered."); + } +} + + +bool I2SAudioComponent::claim_access_(uint8_t access){ + bool success = false; + this->lock(); + if( this->access_mode_ == I2SAccessMode::DUPLEX ){ + this->access_state_ |= access; + success = true; + } + else { + if( this->access_state_ == I2SAccess::FREE ){ + this->access_state_ = access; + } + success = this->access_state_ & access; + } + this->unlock(); + return success; + } + +bool I2SAudioComponent::release_access_(uint8_t access){ + this->lock(); + this->access_state_ = this->access_state_ & (~access); + this->unlock(); + return true; +} + +bool I2SAudioComponent::install_i2s_driver_(i2s_driver_config_t i2s_cfg, uint8_t access){ + bool success = false; + this->lock(); + esph_log_d(TAG, "Install driver requested by %s", access == I2SAccess::RX ? "Reader" : "Writer"); + if( this->access_state_ == I2SAccess::FREE || this->access_state_ == access ){ + if( this->driver_loaded_ ){ + ESP_LOGW(TAG,"trying to load i2s driver twice"); + return true; + } + if(this->access_mode_ == I2SAccessMode::DUPLEX){ + i2s_cfg.mode = (i2s_mode_t) (i2s_cfg.mode | I2S_MODE_TX | I2S_MODE_RX); + } + success = ESP_OK == i2s_driver_install(this->get_port(), &i2s_cfg, 0, nullptr); + esph_log_d(TAG, "Installing driver : %s", success ? "yes" : "no" ); + i2s_pin_config_t pin_config = this->get_pin_config(); + if( success ){ + this->driver_loaded_ = true; + if( this->audio_in_ != nullptr ) + { + pin_config.data_in_num = this->audio_in_->get_din_pin(); + } + if( this->audio_out_ != nullptr ) + { + pin_config.data_out_num = this->audio_out_->get_dout_pin(); + } + success &= ESP_OK == i2s_set_pin(this->get_port(), &pin_config); + if( success ){ + this->installed_cfg_ = i2s_cfg; + } + } + } else if (this->access_mode_ == I2SAccessMode::DUPLEX && this->driver_loaded_ ){ + success = this->validate_cfg_for_duplex_(i2s_cfg); + if (!success ){ + ESP_LOGE(TAG, "incompatible i2s settings for duplex mode, access_state: %d", this->access_state_); + } + } else { + ESP_LOGE(TAG, "Unexpected i2s state: mode: %d access_state: %d access_request: %d", (int) this->access_mode_, (int) this->access_state_, (int) access); + } + this->unlock(); + return success; +} + +bool I2SAudioComponent::uninstall_i2s_driver_(uint8_t access){ + bool success = false; + this->lock(); + // check that i2s is not occupied by others + if( (this->access_state_ & access) == access && (this->access_state_ & ~access) == 0 ){ + i2s_zero_dma_buffer(this->get_port()); + esp_err_t err = i2s_driver_uninstall(this->get_port()); + if (err == ESP_OK) { + success = true; + this->access_state_ = I2SAccess::FREE; + this->driver_loaded_ = false; + } else { + esph_log_e(TAG, "Couldn't unload driver"); + } + } + else { + // other component hasn't released yet + // don't uninstall driver, just release caller + esph_log_d(TAG, "Other component hasn't released"); + this->access_state_ = this->access_state_ & (~access); + } + this->unlock(); + return success; +} + +bool I2SAudioComponent::validate_cfg_for_duplex_(i2s_driver_config_t& i2s_cfg){ + i2s_driver_config_t& installed = this->installed_cfg_; + return ( + installed.sample_rate == i2s_cfg.sample_rate + && installed.bits_per_chan == i2s_cfg.bits_per_chan + ); +} + + +void I2SSettings::dump_i2s_settings() const { + std::string init_str = this->is_fixed_ ? "Fixed-CFG" : "Initial-CFG"; + if( this->i2s_access_ == I2SAccess::RX ){ + esph_log_config(TAG, "I2S-Reader (%s):", init_str.c_str()); + } + else{ + esph_log_config(TAG, "I2S-Writer (%s):", init_str.c_str()); + } + esph_log_config(TAG, " clk_mode: %s", this->i2s_clk_mode_ == I2S_MODE_MASTER ? "internal" : "external" ); + esph_log_config(TAG, " sample-rate: %d bits_per_sample: %d", this->sample_rate_, this->bits_per_sample_ ); + esph_log_config(TAG, " channel_fmt: %d channels: %d", this->channel_fmt_, this->num_of_channels() ); + esph_log_config(TAG, " use_apll: %s, use_pdm: %s", this->use_apll_ ? "yes": "no", this->pdm_ ? "yes": "no"); +} + + +i2s_driver_config_t I2SSettings::get_i2s_cfg() const { + uint8_t mode = this->i2s_clk_mode_ | ( this->i2s_access_ == I2SAccess::RX ? I2S_MODE_RX : I2S_MODE_TX); + + if( this->pdm_){ + mode = (i2s_mode_t) (mode | I2S_MODE_PDM); + } + + i2s_driver_config_t config = { + .mode = (i2s_mode_t) mode, + .sample_rate = this->sample_rate_, + .bits_per_sample = this->bits_per_sample_, + .channel_format = this->channel_fmt_, + .communication_format = I2S_COMM_FORMAT_STAND_I2S, + .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, + .dma_buf_count = 6, + .dma_buf_len = 512, + .use_apll = false, + .tx_desc_auto_clear = true, + .fixed_mclk = I2S_PIN_NO_CHANGE, + .mclk_multiple = I2S_MCLK_MULTIPLE_DEFAULT, + .bits_per_chan = I2S_BITS_PER_CHAN_DEFAULT, +#if SOC_I2S_SUPPORTS_TDM + .chan_mask = I2S_CHANNEL_MONO, + .total_chan = 0, + .left_align = false, + .big_edin = false, + .bit_order_msb = false, + .skip_msk = false, +#endif + }; + + return config; +} + +} // namespace i2s_audio +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/i2s_audio/i2s_audio.h b/esphome/components/i2s_audio/i2s_audio.h new file mode 100644 index 00000000..f45cd85f --- /dev/null +++ b/esphome/components/i2s_audio/i2s_audio.h @@ -0,0 +1,170 @@ +#pragma once + +#include "esphome/core/defines.h" +#ifdef USE_ESP32 + +#include +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace i2s_audio { + +enum class I2SAccessMode : uint8_t {EXCLUSIVE, DUPLEX}; +class I2SAccess { +public: + static constexpr uint8_t FREE = 0; + static constexpr uint8_t RX = 1; + static constexpr uint8_t TX = 2; +}; + + +class I2SReader; +class I2SWriter; +class I2SAudioComponent : public Component { + public: + void setup() override; + void dump_config() override; + + i2s_pin_config_t get_pin_config() const { + return { + .mck_io_num = this->mclk_pin_, + .bck_io_num = this->bclk_pin_, + .ws_io_num = this->lrclk_pin_, + .data_out_num = I2S_PIN_NO_CHANGE, + .data_in_num = I2S_PIN_NO_CHANGE, + }; + } + + void set_mclk_pin(int pin) { this->mclk_pin_ = pin; } + void set_bclk_pin(int pin) { this->bclk_pin_ = pin; } + void set_lrclk_pin(int pin) { this->lrclk_pin_ = pin; } + + void lock() { this->lock_.lock(); } + bool try_lock() { return this->lock_.try_lock(); } + void unlock() { this->lock_.unlock(); } + + i2s_port_t get_port() const { return this->port_; } + void set_audio_in(I2SReader* comp_in){ this->audio_in_ = comp_in;} + void set_audio_out(I2SWriter* comp_out){ this->audio_out_ = comp_out;} + + void set_access_mode(I2SAccessMode access_mode){this->access_mode_ = access_mode;} + bool is_exclusive(){return this->access_mode_ == I2SAccessMode::EXCLUSIVE;} + + protected: + friend I2SReader; + friend I2SWriter; + + Mutex lock_; + I2SAccessMode access_mode_{I2SAccessMode::DUPLEX}; + uint8_t access_state_{I2SAccess::FREE}; + + bool claim_access_(uint8_t access); + bool release_access_(uint8_t access); + bool install_i2s_driver_(i2s_driver_config_t i2s_cfg, uint8_t access); + bool uninstall_i2s_driver_(uint8_t access); + bool validate_cfg_for_duplex_(i2s_driver_config_t& i2s_cfg); + + I2SReader *audio_in_{nullptr}; + I2SWriter *audio_out_{nullptr}; + + int mclk_pin_{I2S_PIN_NO_CHANGE}; + int bclk_pin_{I2S_PIN_NO_CHANGE}; + int lrclk_pin_; + i2s_port_t port_{}; + i2s_driver_config_t installed_cfg_{}; + bool driver_loaded_{false}; +}; + +class I2SSettings { +public: + I2SSettings() = default; + I2SSettings(uint8_t access) : i2s_access_(access) {} + + i2s_driver_config_t get_i2s_cfg() const; + void dump_i2s_settings() const; + + void set_use_apll(uint32_t use_apll) { this->use_apll_ = use_apll; } + void set_bits_per_sample(i2s_bits_per_sample_t bits_per_sample) { this->bits_per_sample_ = bits_per_sample; } + void set_channel(i2s_channel_fmt_t channel_fmt) { this->channel_fmt_ = channel_fmt; } + void set_pdm(bool pdm) { this->pdm_ = pdm; } + void set_sample_rate(uint32_t sample_rate) { this->sample_rate_ = sample_rate; } + void set_fixed_settings(bool is_fixed){ this->is_fixed_ = is_fixed; } + int num_of_channels() const { return (this->channel_fmt_ == I2S_CHANNEL_FMT_ONLY_RIGHT + || this->channel_fmt_ == I2S_CHANNEL_FMT_ONLY_LEFT) ? 1 : 2; } + void set_clk_mode(i2s_mode_t clk_mode){ this->i2s_clk_mode_ = clk_mode; } + +protected: + bool use_apll_{false}; + i2s_bits_per_sample_t bits_per_sample_; + i2s_channel_fmt_t channel_fmt_; + i2s_mode_t i2s_clk_mode_{I2S_MODE_MASTER}; + i2s_mode_t i2s_access_mode_; + bool pdm_{false}; + uint32_t sample_rate_; + + bool is_fixed_{false}; + uint8_t i2s_access_; +}; + + +class I2SReader : public I2SSettings, public Parented { +public: + I2SReader() : I2SSettings( I2SAccess::RX ) {} + + bool install_i2s_driver(i2s_driver_config_t i2s_cfg){ + return this->parent_->install_i2s_driver_(i2s_cfg, I2SAccess::RX);} + bool uninstall_i2s_driver(){ return this->parent_->uninstall_i2s_driver_(I2SAccess::RX);} + bool claim_i2s_access(){return this->parent_->claim_access_(I2SAccess::RX);} + bool release_i2s_access(){return this->parent_->release_access_(I2SAccess::RX);} + bool is_adjustable(){return !this->is_fixed_ && this->parent_->is_exclusive();} +#if SOC_I2S_SUPPORTS_ADC + void set_adc_channel(adc1_channel_t channel) { + this->adc_channel_ = channel; + this->use_internal_adc_ = true; + } +#endif + void set_din_pin(int8_t pin) { this->din_pin_ = pin; } + int8_t get_din_pin() { return this->din_pin_; } + +protected: +#if SOC_I2S_SUPPORTS_ADC + adc1_channel_t adc_channel_{ADC1_CHANNEL_MAX}; + bool use_internal_adc_{false}; +#endif + int8_t din_pin_{I2S_PIN_NO_CHANGE}; +}; + + +class I2SWriter : public I2SSettings, public Parented { +public: + I2SWriter() : I2SSettings(I2SAccess::TX ) {} + + bool install_i2s_driver(i2s_driver_config_t i2s_cfg){ + return this->parent_->install_i2s_driver_(i2s_cfg, I2SAccess::TX); } + bool uninstall_i2s_driver(){ return this->parent_->uninstall_i2s_driver_(I2SAccess::TX);} + bool claim_i2s_access(){return this->parent_->claim_access_(I2SAccess::TX);} + bool release_i2s_access(){return this->parent_->release_access_(I2SAccess::TX);} + bool is_adjustable(){return !this->is_fixed_ && this->parent_->is_exclusive();} + +#if SOC_I2S_SUPPORTS_DAC + void set_internal_dac_mode(i2s_dac_mode_t mode) { this->internal_dac_mode_ = mode; } +#endif + + void set_dout_pin(int8_t pin) { this->dout_pin_ = pin; } + int8_t get_dout_pin() { return this->dout_pin_; } + +protected: +#if SOC_I2S_SUPPORTS_DAC + i2s_dac_mode_t internal_dac_mode_{I2S_DAC_CHANNEL_DISABLE}; +#endif + int8_t dout_pin_{I2S_PIN_NO_CHANGE}; +}; + + + + +} // namespace i2s_audio +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/i2s_audio/i2s_settings.py b/esphome/components/i2s_audio/i2s_settings.py new file mode 100644 index 00000000..b953c261 --- /dev/null +++ b/esphome/components/i2s_audio/i2s_settings.py @@ -0,0 +1,87 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_CHANNEL + +CONF_CLK_MODE = "i2s_clock_mode" +CONF_SAMPLE_RATE = "sample_rate" +CONF_BITS_PER_SAMPLE = "bits_per_sample" +CONF_PDM = "pdm" +CONF_USE_APLL = "use_apll" +CONF_FIXED_SETTINGS = "fixed_settings" + + +INTERNAL_CLK = "internal" +EXTERNAL_CLK = "external" +i2s_mode_t = cg.global_ns.enum("i2s_mode_t") +I2S_CLK_MODES = { + INTERNAL_CLK: i2s_mode_t.I2S_MODE_MASTER, # NOLINT + EXTERNAL_CLK: i2s_mode_t.I2S_MODE_SLAVE, # NOLINT +} + + +i2s_dac_mode_t = cg.global_ns.enum("i2s_dac_mode_t") +INTERNAL_DAC_OPTIONS = { + "left": i2s_dac_mode_t.I2S_DAC_CHANNEL_LEFT_EN, + "right": i2s_dac_mode_t.I2S_DAC_CHANNEL_RIGHT_EN, + "stereo": i2s_dac_mode_t.I2S_DAC_CHANNEL_BOTH_EN, +} + +i2s_channel_fmt_t = cg.global_ns.enum("i2s_channel_fmt_t") +CHANNEL_FORMAT = { + # Only load data in left channel (mono mode) + "left": i2s_channel_fmt_t.I2S_CHANNEL_FMT_ONLY_LEFT, + # Only load data in right channel (mono mode) + "right": i2s_channel_fmt_t.I2S_CHANNEL_FMT_ONLY_RIGHT, + # Separated left and right channel + "right_left": i2s_channel_fmt_t.I2S_CHANNEL_FMT_RIGHT_LEFT, + # Load right channel data in both two channels + "all_right": i2s_channel_fmt_t.I2S_CHANNEL_FMT_ALL_RIGHT, + # Load left channel data in both two channels + "all_left": i2s_channel_fmt_t.I2S_CHANNEL_FMT_ALL_LEFT, +} + +i2s_bits_per_sample_t = cg.global_ns.enum("i2s_bits_per_sample_t") +BITS_PER_SAMPLE = { + 16: i2s_bits_per_sample_t.I2S_BITS_PER_SAMPLE_16BIT, + 24: i2s_bits_per_sample_t.I2S_BITS_PER_SAMPLE_24BIT, + 32: i2s_bits_per_sample_t.I2S_BITS_PER_SAMPLE_32BIT, +} + +i2s_bits_per_chan_t = cg.global_ns.enum("i2s_bits_per_chan_t") +BITS_PER_CHANNEL = { + "default": i2s_bits_per_chan_t.I2S_BITS_PER_CHAN_DEFAULT, + 8: i2s_bits_per_chan_t.I2S_BITS_PER_CHAN_8BIT, + 16: i2s_bits_per_chan_t.I2S_BITS_PER_CHAN_16BIT, + 24: i2s_bits_per_chan_t.I2S_BITS_PER_CHAN_24BIT, + 32: i2s_bits_per_chan_t.I2S_BITS_PER_CHAN_32BIT, +} + +_validate_bits = cv.float_with_unit("bits", "bit") + +CONFIG_SCHEMA_I2S_COMMON = cv.Schema( + { + cv.Optional(CONF_CLK_MODE, default=INTERNAL_CLK): cv.enum(I2S_CLK_MODES), + cv.Optional(CONF_CHANNEL, default="right_left"): cv.enum(CHANNEL_FORMAT), + cv.Optional(CONF_SAMPLE_RATE, default=16000): cv.int_range(min=1), + cv.Optional(CONF_BITS_PER_SAMPLE, default="32bit"): cv.All( + _validate_bits, cv.enum(BITS_PER_SAMPLE) + ), + cv.Optional(CONF_USE_APLL, default=False): cv.boolean, + cv.Optional(CONF_FIXED_SETTINGS, default=False): cv.boolean, + } +) + + +def get_i2s_config_schema(default_channel, default_rate, default_bits): + return cv.Schema( + { + cv.Optional(CONF_CLK_MODE, default=INTERNAL_CLK): cv.enum(I2S_CLK_MODES), + cv.Optional(CONF_CHANNEL, default=default_channel): cv.enum(CHANNEL_FORMAT), + cv.Optional(CONF_SAMPLE_RATE, default=default_rate): cv.int_range(min=1), + cv.Optional(CONF_BITS_PER_SAMPLE, default=default_bits): cv.All( + _validate_bits, cv.enum(BITS_PER_SAMPLE) + ), + cv.Optional(CONF_USE_APLL, default=False): cv.boolean, + cv.Optional(CONF_FIXED_SETTINGS, default=False): cv.boolean, + } + ) diff --git a/esphome/components/i2s_audio/media_player/__init__.py b/esphome/components/i2s_audio/media_player/__init__.py new file mode 100644 index 00000000..7158562d --- /dev/null +++ b/esphome/components/i2s_audio/media_player/__init__.py @@ -0,0 +1,5 @@ +import esphome.config_validation as cv + +CONFIG_SCHEMA = cv.invalid( + "The arduino media player is not supported in this custom component." +) diff --git a/esphome/components/i2s_audio/microphone/__init__.py b/esphome/components/i2s_audio/microphone/__init__.py new file mode 100644 index 00000000..bcb2e418 --- /dev/null +++ b/esphome/components/i2s_audio/microphone/__init__.py @@ -0,0 +1,120 @@ +import esphome.config_validation as cv +import esphome.codegen as cg + +from esphome import pins +from esphome.const import CONF_CHANNEL, CONF_ID, CONF_MODEL, CONF_NUMBER +from esphome.components import microphone, esp32 +from esphome.components.adc import ESP32_VARIANT_ADC1_PIN_TO_CHANNEL, validate_adc_pin + +from .. import i2s_settings as i2s + +from .. import ( + i2s_audio_ns, + I2SAudioComponent, + I2SReader, + CONF_I2S_ADC, + CONF_I2S_AUDIO_ID, + CONF_I2S_DIN_PIN, + CONFIG_SCHEMA_ADC, + register_i2s_reader, +) + +CODEOWNERS = ["@jesserockz"] +DEPENDENCIES = ["i2s_audio"] + +CONF_ADC_PIN = "adc_pin" +CONF_ADC_TYPE = "adc_type" +CONF_PDM = "pdm" +CONF_SAMPLE_RATE = "sample_rate" +CONF_BITS_PER_SAMPLE = "bits_per_sample" +CONF_USE_APLL = "use_apll" + +I2SAudioMicrophone = i2s_audio_ns.class_( + "I2SAudioMicrophone", I2SReader, microphone.Microphone, cg.Component +) + +i2s_channel_fmt_t = cg.global_ns.enum("i2s_channel_fmt_t") +CHANNELS = { + "left": i2s_channel_fmt_t.I2S_CHANNEL_FMT_ONLY_LEFT, + "right": i2s_channel_fmt_t.I2S_CHANNEL_FMT_ONLY_RIGHT, +} +i2s_bits_per_sample_t = cg.global_ns.enum("i2s_bits_per_sample_t") +BITS_PER_SAMPLE = { + 16: i2s_bits_per_sample_t.I2S_BITS_PER_SAMPLE_16BIT, + 32: i2s_bits_per_sample_t.I2S_BITS_PER_SAMPLE_32BIT, +} + +INTERNAL_ADC_VARIANTS = [esp32.const.VARIANT_ESP32] +PDM_VARIANTS = [esp32.const.VARIANT_ESP32, esp32.const.VARIANT_ESP32S3] + +_validate_bits = cv.float_with_unit("bits", "bit") + + +def validate_esp32_variant(config): + variant = esp32.get_esp32_variant() + if config[CONF_ADC_TYPE] == "external": + if config[CONF_PDM]: + if variant not in PDM_VARIANTS: + raise cv.Invalid(f"{variant} does not support PDM") + return config + if config[CONF_ADC_TYPE] == "internal": + if variant not in INTERNAL_ADC_VARIANTS: + raise cv.Invalid(f"{variant} does not have an internal ADC") + return config + raise NotImplementedError + + +BASE_SCHEMA = microphone.MICROPHONE_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(I2SAudioMicrophone), + cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent), + cv.Optional(CONF_CHANNEL, default="right"): cv.enum(CHANNELS), + cv.Optional(CONF_SAMPLE_RATE, default=16000): cv.int_range(min=1), + cv.Optional(CONF_BITS_PER_SAMPLE, default="32bit"): cv.All( + _validate_bits, cv.enum(BITS_PER_SAMPLE) + ), + cv.Optional(CONF_USE_APLL, default=False): cv.boolean, + } +).extend(cv.COMPONENT_SCHEMA) + +CONFIG_SCHEMA = cv.All( + cv.typed_schema( + { + "internal": BASE_SCHEMA.extend( + { + cv.Required(CONF_ADC_PIN): validate_adc_pin, + } + ), + "external": BASE_SCHEMA.extend( + { + cv.Required(CONF_I2S_DIN_PIN): pins.internal_gpio_input_pin_number, + cv.Required(CONF_PDM): cv.boolean, + cv.Optional( + CONF_I2S_ADC, default={CONF_MODEL: "generic"} + ): CONFIG_SCHEMA_ADC, + } + ).extend(i2s.CONFIG_SCHEMA_I2S_COMMON), + }, + key=CONF_ADC_TYPE, + ), + validate_esp32_variant, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + # await cg.register_parented(var, config[CONF_I2S_AUDIO_ID]) + + if config[CONF_ADC_TYPE] == "internal": + variant = esp32.get_esp32_variant() + pin_num = config[CONF_ADC_PIN][CONF_NUMBER] + channel = ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant][pin_num] + cg.add(var.set_adc_channel(channel)) + else: + # cg.add(var.set_din_pin(config[CONF_I2S_DIN_PIN])) + # cg.add(var.set_pdm(config[CONF_PDM])) + await register_i2s_reader(var, config) + + await microphone.register_microphone(var, config) diff --git a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp new file mode 100644 index 00000000..20baa4b9 --- /dev/null +++ b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp @@ -0,0 +1,169 @@ +#include "i2s_audio_microphone.h" + +#ifdef USE_ESP32 + +#include +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +#ifdef I2S_EXTERNAL_ADC +#include "../external_adc.h" +#endif + +namespace esphome { +namespace i2s_audio { + +static const size_t BUFFER_SIZE = 512; + +static const char *const TAG = "i2s_audio.microphone"; + +void I2SAudioMicrophone::setup() { + ESP_LOGCONFIG(TAG, "Setting up I2S Audio Microphone..."); +#if SOC_I2S_SUPPORTS_ADC + if (this->use_internal_adc_) { + if (this->parent_->get_port() != I2S_NUM_0) { + ESP_LOGE(TAG, "Internal ADC only works on I2S0!"); + this->mark_failed(); + return; + } + } else +#endif + if (this->pdm_) { + if (this->parent_->get_port() != I2S_NUM_0) { + ESP_LOGE(TAG, "PDM only works on I2S0!"); + this->mark_failed(); + return; + } + } +} +void I2SAudioMicrophone::dump_config() { + this->dump_i2s_settings(); +} + +void I2SAudioMicrophone::start() { + if (this->is_failed()) + return; + if (this->state_ == microphone::STATE_RUNNING) + return; // Already running + this->state_ = microphone::STATE_STARTING; +} +void I2SAudioMicrophone::start_() { + if (!this->claim_i2s_access()) { + return; // Waiting for another i2s to return lock + } + +#ifdef I2S_EXTERNAL_ADC + if( this->external_adc_ != nullptr ){ + this->external_adc_->init_device(); + } +#endif + +i2s_driver_config_t config = this->get_i2s_cfg(); +//config.mode = (i2s_mode_t) (I2S_MODE_MASTER | I2S_MODE_RX); + +#if SOC_I2S_SUPPORTS_ADC + if (this->use_internal_adc_) { + config.mode = (i2s_mode_t) (config.mode | I2S_MODE_ADC_BUILT_IN); + i2s_driver_install(this->parent_->get_port(), &config, 0, nullptr); + + i2s_set_adc_mode(ADC_UNIT_1, this->adc_channel_); + i2s_adc_enable(this->parent_->get_port()); + } else +#endif + { + if (this->pdm_) + config.mode = (i2s_mode_t) (config.mode | I2S_MODE_PDM); + + this->install_i2s_driver(config); + + } + +#ifdef I2S_EXTERNAL_ADC + if( this->external_adc_ != nullptr ){ + this->external_adc_->apply_i2s_settings(config); + } +#endif + + this->state_ = microphone::STATE_RUNNING; + this->high_freq_.start(); +} + +void I2SAudioMicrophone::stop() { + if (this->state_ == microphone::STATE_STOPPED || this->is_failed()) + return; + if (this->state_ == microphone::STATE_STARTING) { + this->state_ = microphone::STATE_STOPPED; + return; + } + this->state_ = microphone::STATE_STOPPING; +} + +void I2SAudioMicrophone::stop_() { + this->uninstall_i2s_driver(); + this->release_i2s_access(); + this->state_ = microphone::STATE_STOPPED; + this->high_freq_.stop(); +} + +size_t I2SAudioMicrophone::read(int16_t *buf, size_t len) { + size_t bytes_read = 0; + esp_err_t err = i2s_read(this->parent_->get_port(), buf, len, &bytes_read, (1 / portTICK_PERIOD_MS)); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Error reading from I2S microphone: %s", esp_err_to_name(err)); + this->status_set_warning(); + return 0; + } + + if (bytes_read == 0) { + return 0; + } + this->status_clear_warning(); + if (this->bits_per_sample_ == I2S_BITS_PER_SAMPLE_16BIT) { + return bytes_read; + } else if (this->bits_per_sample_ == I2S_BITS_PER_SAMPLE_32BIT) { + std::vector samples; + size_t samples_read = bytes_read / sizeof(int32_t); + uint8_t shift = 16 - this->gain_log2_ ; + samples.resize(samples_read); + for (size_t i = 0; i < samples_read; i++) { + int32_t temp = reinterpret_cast(buf)[i] >> shift; + samples[i] = static_cast(clamp(temp, INT16_MIN, INT16_MAX)); + } + memcpy(buf, samples.data(), samples_read * sizeof(int16_t)); + return samples_read * sizeof(int16_t); + } else { + ESP_LOGE(TAG, "Unsupported bits per sample: %d", this->bits_per_sample_); + return 0; + } +} + +void I2SAudioMicrophone::read_() { + std::vector samples; + samples.resize(BUFFER_SIZE); + size_t bytes_read = this->read(samples.data(), BUFFER_SIZE / sizeof(int16_t)); + samples.resize(bytes_read / sizeof(int16_t)); + this->data_callbacks_.call(samples); +} + +void I2SAudioMicrophone::loop() { + switch (this->state_) { + case microphone::STATE_STOPPED: + break; + case microphone::STATE_STARTING: + this->start_(); + break; + case microphone::STATE_RUNNING: + if (this->data_callbacks_.size() > 0) { + this->read_(); + } + break; + case microphone::STATE_STOPPING: + this->stop_(); + break; + } +} + +} // namespace i2s_audio +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h new file mode 100644 index 00000000..b95d3d80 --- /dev/null +++ b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h @@ -0,0 +1,37 @@ +#pragma once + +#ifdef USE_ESP32 + +#include "../i2s_audio.h" + +#include "esphome/components/microphone/microphone.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace i2s_audio { + +class I2SAudioMicrophone : public I2SReader, public microphone::Microphone, public Component { + public: + void setup() override; + void start() override; + void stop() override; + + void loop() override; + void dump_config() override; + + size_t read(int16_t *buf, size_t len) override; + void set_gain_log2(uint8_t gain_log2){this->gain_log2_ = gain_log2;} + + protected: + void start_(); + void stop_(); + void read_(); + + uint8_t gain_log2_{2}; + HighFrequencyLoopRequester high_freq_; +}; + +} // namespace i2s_audio +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/i2s_audio/speaker/__init__.py b/esphome/components/i2s_audio/speaker/__init__.py new file mode 100644 index 00000000..71d9d711 --- /dev/null +++ b/esphome/components/i2s_audio/speaker/__init__.py @@ -0,0 +1,95 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.const import CONF_ID, CONF_MODE, CONF_MODEL +from esphome.components import esp32, speaker + +from .. import i2s_settings as i2s + +from .. import ( + CONF_I2S_AUDIO_ID, + CONF_I2S_DOUT_PIN, + CONF_I2S_DAC, + CONFIG_SCHEMA_DAC, + I2SAudioComponent, + I2SWriter, + i2s_audio_ns, + register_i2s_writer, +) + +CODEOWNERS = ["@jesserockz"] +DEPENDENCIES = ["i2s_audio"] + +I2SAudioSpeaker = i2s_audio_ns.class_( + "I2SAudioSpeaker", cg.Component, speaker.Speaker, I2SWriter +) + +i2s_dac_mode_t = cg.global_ns.enum("i2s_dac_mode_t") + +CONF_MUTE_PIN = "mute_pin" +CONF_DAC_TYPE = "dac_type" + +INTERNAL_DAC_OPTIONS = { + "left": i2s_dac_mode_t.I2S_DAC_CHANNEL_LEFT_EN, + "right": i2s_dac_mode_t.I2S_DAC_CHANNEL_RIGHT_EN, + "stereo": i2s_dac_mode_t.I2S_DAC_CHANNEL_BOTH_EN, +} + +NO_INTERNAL_DAC_VARIANTS = [esp32.const.VARIANT_ESP32S2] + + +def validate_esp32_variant(config): + if config[CONF_DAC_TYPE] != "internal": + return config + variant = esp32.get_esp32_variant() + if variant in NO_INTERNAL_DAC_VARIANTS: + raise cv.Invalid(f"{variant} does not have an internal DAC") + return config + + +CONFIG_SCHEMA = cv.All( + cv.typed_schema( + { + "internal": speaker.SPEAKER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(I2SAudioSpeaker), + cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent), + cv.Required(CONF_MODE): cv.enum(INTERNAL_DAC_OPTIONS, lower=True), + } + ).extend(cv.COMPONENT_SCHEMA), + "external": speaker.SPEAKER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(I2SAudioSpeaker), + cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent), + cv.Required( + CONF_I2S_DOUT_PIN + ): pins.internal_gpio_output_pin_number, + cv.Optional( + CONF_I2S_DAC, default={CONF_MODEL: "generic"} + ): CONFIG_SCHEMA_DAC, + } + ) + .extend( + i2s.get_i2s_config_schema( + default_channel="right", default_rate=16000, default_bits="16bit" + ) + ) + .extend(cv.COMPONENT_SCHEMA), + }, + key=CONF_DAC_TYPE, + ), + validate_esp32_variant, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await speaker.register_speaker(var, config) + + await cg.register_parented(var, config[CONF_I2S_AUDIO_ID]) + + if config[CONF_DAC_TYPE] == "internal": + cg.add(var.set_internal_dac_mode(config[CONF_MODE])) + else: + await register_i2s_writer(var, config) diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp new file mode 100644 index 00000000..b40a2177 --- /dev/null +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp @@ -0,0 +1,250 @@ +#include "i2s_audio_speaker.h" + +#ifdef USE_ESP32 + +#include +#include "esphome/core/application.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +#ifdef I2S_EXTERNAL_DAC +#include "../external_dac.h" +#endif +namespace esphome { +namespace i2s_audio { + +static const size_t BUFFER_COUNT = 20; + +static const char *const TAG = "i2s_audio.speaker"; + +void I2SAudioSpeaker::setup() { + ESP_LOGCONFIG(TAG, "Setting up I2S Audio Speaker..."); + + this->buffer_queue_ = xQueueCreate(BUFFER_COUNT, sizeof(DataEvent)); + if (this->buffer_queue_ == nullptr) { + ESP_LOGE(TAG, "Failed to create buffer queue"); + this->mark_failed(); + return; + } + this->event_queue_ = xQueueCreate(BUFFER_COUNT, sizeof(TaskEvent)); + if (this->event_queue_ == nullptr) { + ESP_LOGE(TAG, "Failed to create event queue"); + this->mark_failed(); + return; + } +} + +void I2SAudioSpeaker::dump_config() { + this->dump_i2s_settings(); +} + +void I2SAudioSpeaker::start() { + if (this->is_failed()) { + ESP_LOGE(TAG, "Cannot start audio, speaker failed to setup"); + return; + } + if (this->task_created_) { + ESP_LOGW(TAG, "Called start while task has been already created."); + return; + } + this->state_ = speaker::STATE_STARTING; +} + +void I2SAudioSpeaker::start_() { + if (this->task_created_) { + return; + } + if (!this->claim_i2s_access()) { + return; // Waiting for another i2s component to return lock + } + + xTaskCreate(I2SAudioSpeaker::player_task, "speaker_task", 8192, (void *) this, 1, &this->player_task_handle_); + this->task_created_ = true; +} + +void I2SAudioSpeaker::player_task(void *params) { + I2SAudioSpeaker *this_speaker = (I2SAudioSpeaker *) params; + +#ifdef I2S_EXTERNAL_DAC + if( this_speaker->external_dac_ != nullptr ){ + this_speaker->external_dac_->init_device(); + } +#endif + + TaskEvent event; + event.type = TaskEventType::STARTING; + xQueueSend(this_speaker->event_queue_, &event, portMAX_DELAY); + + i2s_driver_config_t config = this_speaker->get_i2s_cfg(); + config.mode = (i2s_mode_t) (I2S_MODE_MASTER | I2S_MODE_TX); + +#if SOC_I2S_SUPPORTS_DAC + if (this_speaker->internal_dac_mode_ != I2S_DAC_CHANNEL_DISABLE) { + config.mode = (i2s_mode_t) (config.mode | I2S_MODE_DAC_BUILT_IN); + } +#endif + + bool success = this_speaker->install_i2s_driver(config); + if (!success) { + event.type = TaskEventType::WARNING; + event.err = -2; + xQueueSend(this_speaker->event_queue_, &event, 0); + event.type = TaskEventType::STOPPED; + xQueueSend(this_speaker->event_queue_, &event, 0); + while (true) { + delay(10); + } + } + +#if SOC_I2S_SUPPORTS_DAC + if (this_speaker->internal_dac_mode_ != I2S_DAC_CHANNEL_DISABLE) { + i2s_set_dac_mode(this_speaker->internal_dac_mode_); + } +#endif + +#ifdef I2S_EXTERNAL_DAC + if( this_speaker->external_dac_ != nullptr ){ + this_speaker->external_dac_->apply_i2s_settings(config); + this_speaker->external_dac_->reset_volume(); + } +#endif + + DataEvent data_event; + + event.type = TaskEventType::STARTED; + xQueueSend(this_speaker->event_queue_, &event, portMAX_DELAY); + + while (true) { + if (xQueueReceive(this_speaker->buffer_queue_, &data_event, 1000 / portTICK_PERIOD_MS) != pdTRUE) { + break; // End of audio from main thread + } + if (data_event.stop) { + // Stop signal from main thread + xQueueReset(this_speaker->buffer_queue_); // Flush queue + break; + } + + size_t bytes_written; + esp_err_t err = i2s_write(this_speaker->parent_->get_port(), data_event.data, data_event.len, &bytes_written, + (32 / portTICK_PERIOD_MS)); + if (err != ESP_OK) { + event = {.type = TaskEventType::WARNING, .err = err}; + xQueueSend(this_speaker->event_queue_, &event, portMAX_DELAY); + continue; + } + + event.type = TaskEventType::PLAYING; + xQueueSend(this_speaker->event_queue_, &event, portMAX_DELAY); + } + + i2s_zero_dma_buffer(this_speaker->parent_->get_port()); + + event.type = TaskEventType::STOPPING; + xQueueSend(this_speaker->event_queue_, &event, portMAX_DELAY); + + this_speaker->uninstall_i2s_driver(); + + event.type = TaskEventType::STOPPED; + xQueueSend(this_speaker->event_queue_, &event, portMAX_DELAY); + + while (true) { + delay(10); + } +} + +void I2SAudioSpeaker::stop() { + if (this->is_failed()) + return; + if (this->state_ == speaker::STATE_STOPPED) + return; + if (this->state_ == speaker::STATE_STARTING) { + this->state_ = speaker::STATE_STOPPED; + return; + } + this->state_ = speaker::STATE_STOPPING; + DataEvent data; + data.stop = true; + xQueueSendToFront(this->buffer_queue_, &data, portMAX_DELAY); +} + +void I2SAudioSpeaker::watch_() { + TaskEvent event; + if (xQueueReceive(this->event_queue_, &event, 0) == pdTRUE) { + switch (event.type) { + case TaskEventType::STARTING: + ESP_LOGD(TAG, "Starting I2S Audio Speaker"); + break; + case TaskEventType::STARTED: + ESP_LOGD(TAG, "Started I2S Audio Speaker"); + this->state_ = speaker::STATE_RUNNING; + break; + case TaskEventType::STOPPING: + ESP_LOGD(TAG, "Stopping I2S Audio Speaker"); + break; + case TaskEventType::PLAYING: + this->status_clear_warning(); + break; + case TaskEventType::STOPPED: + this->state_ = speaker::STATE_STOPPED; + vTaskDelete(this->player_task_handle_); + this->task_created_ = false; + this->player_task_handle_ = nullptr; + this->release_i2s_access(); + xQueueReset(this->buffer_queue_); + ESP_LOGD(TAG, "Stopped I2S Audio Speaker"); + break; + case TaskEventType::WARNING: + ESP_LOGW(TAG, "Error writing to I2S: %s", esp_err_to_name(event.err)); + this->status_set_warning(); + break; + } + } +} + +void I2SAudioSpeaker::loop() { + switch (this->state_) { + case speaker::STATE_STARTING: + this->start_(); + this->watch_(); + break; + case speaker::STATE_RUNNING: + case speaker::STATE_STOPPING: + this->watch_(); + break; + case speaker::STATE_STOPPED: + break; + } +} + +size_t I2SAudioSpeaker::play(const uint8_t *data, size_t length) { + if (this->is_failed()) { + ESP_LOGE(TAG, "Cannot play audio, speaker failed to setup"); + return 0; + } + if (this->state_ != speaker::STATE_RUNNING && this->state_ != speaker::STATE_STARTING) { + this->start(); + } + + size_t remaining = length; + size_t index = 0; + while (remaining > 0) { + DataEvent event; + event.stop = false; + size_t to_send_length = std::min(remaining, BUFFER_SIZE); + event.len = to_send_length; + memcpy(event.data, data + index, to_send_length); + if (xQueueSend(this->buffer_queue_, &event, 0) != pdTRUE) { + return index; + } + remaining -= to_send_length; + index += to_send_length; + } + return index; +} + +bool I2SAudioSpeaker::has_buffered_data() const { return uxQueueMessagesWaiting(this->buffer_queue_) > 0; } + +} // namespace i2s_audio +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h new file mode 100644 index 00000000..c4ef3533 --- /dev/null +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h @@ -0,0 +1,79 @@ +#pragma once + +#ifdef USE_ESP32 + +#include "../i2s_audio.h" + +#include +#include +#include + +#include "esphome/components/speaker/speaker.h" +#include "esphome/core/component.h" +#include "esphome/core/gpio.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace i2s_audio { + +static const size_t BUFFER_SIZE = 1024; + +enum class TaskEventType : uint8_t { + STARTING = 0, + STARTED, + PLAYING, + STOPPING, + STOPPED, + WARNING = 255, +}; + +struct TaskEvent { + TaskEventType type; + esp_err_t err; +}; + +struct DataEvent { + bool stop; + size_t len; + uint8_t data[BUFFER_SIZE]; +}; + +class I2SAudioSpeaker : public Component, public speaker::Speaker, public I2SWriter { + public: + float get_setup_priority() const override { return esphome::setup_priority::LATE; } + + void setup() override; + void loop() override; + void dump_config() override; + +#if SOC_I2S_SUPPORTS_DAC + void set_internal_dac_mode(i2s_dac_mode_t mode) { this->internal_dac_mode_ = mode; } +#endif + + void start() override; + void stop() override; + + size_t play(const uint8_t *data, size_t length) override; + + bool has_buffered_data() const override; + + protected: + void start_(); + void watch_(); + + static void player_task(void *params); + + TaskHandle_t player_task_handle_{nullptr}; + QueueHandle_t buffer_queue_; + QueueHandle_t event_queue_; + + bool task_created_{false}; +#if SOC_I2S_SUPPORTS_DAC + i2s_dac_mode_t internal_dac_mode_{I2S_DAC_CHANNEL_DISABLE}; +#endif +}; + +} // namespace i2s_audio +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/pcm5122/__init__.py b/esphome/components/pcm5122/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/esphome/components/pcm5122/audio_dac.py b/esphome/components/pcm5122/audio_dac.py new file mode 100644 index 00000000..e816f7ca --- /dev/null +++ b/esphome/components/pcm5122/audio_dac.py @@ -0,0 +1,29 @@ +import esphome.codegen as cg +from esphome.components import i2c +import esphome.config_validation as cv +from esphome import automation +from esphome.components.audio_dac import AudioDac, audio_dac_ns +from esphome.const import CONF_ID, CONF_MODE + +CODEOWNERS = ["@gnumpi"] +DEPENDENCIES = ["i2c"] + +pcm5122_ns = cg.esphome_ns.namespace("pcm5122") +PCM5122 = pcm5122_ns.class_("PCM5122", AudioDac, cg.Component, i2c.I2CDevice) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(PCM5122), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x18)) +) + + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) \ No newline at end of file diff --git a/esphome/components/pcm5122/pcm5122.cpp b/esphome/components/pcm5122/pcm5122.cpp new file mode 100644 index 00000000..c03b5c44 --- /dev/null +++ b/esphome/components/pcm5122/pcm5122.cpp @@ -0,0 +1,90 @@ +#include "pcm5122.h" + +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pcm5122 { + +static const char *const TAG = "pcm5122"; + +static const uint8_t PCM5122_REG00_PAGE_SELECT = 0x00; // Page Select + +void PCM5122::setup(){ + // select page 0 + this->reg(PCM5122_REG00_PAGE_SELECT) = 0x00; + + uint8_t chd1 = this->reg(0x09).get(); + uint8_t chd2 = this->reg(0x10).get(); + if( chd1 == 0x00 && chd2 == 0x00 ){ + ESP_LOGD(TAG, "PCM5122 chip found."); + } + else + { + ESP_LOGD(TAG, "PCM5122 chip not found."); + this->mark_failed(); + return; + } + + //RESET + this->reg(0x01) = 0x10; + delay(20); + this->reg(0x01) = 0x00; + + uint8_t err_detect = this->reg(0x25).get(); + //set 'Ignore Clock Halt Detection' + err_detect |= (1 << 3); + //enable Clock Divider Autoset + err_detect &= ~(1 << 1); + this->reg(0x25) = err_detect; + + //set 32bit - I2S + this->reg(0x28) = 3; //32bits + + //001: The PLL reference clock is BCK + uint8_t pll_ref = this->reg(0x0D).get(); + pll_ref &= ~(7 << 4); + pll_ref |= (1 << 4); + this->reg(0x0D) = pll_ref; +} + +void PCM5122::dump_config(){ + +} + +bool PCM5122::set_mute_off(){ + this->is_muted_ = false; + return this->write_mute_(); +} + +bool PCM5122::set_mute_on(){ + this->is_muted_ = true; + return this->write_mute_(); +} + +bool PCM5122::set_volume(float volume) { + this->volume_ = clamp(volume, 0.0, 1.0); + return this->write_volume_(); +} + +bool PCM5122::is_muted() { + return this->is_muted_; +} + +float PCM5122::volume() { + return this->volume_; +} + +bool PCM5122::write_mute_() { + return true; +} + +bool PCM5122::write_volume_() { + return true; +} + + + +} +} \ No newline at end of file diff --git a/esphome/components/pcm5122/pcm5122.h b/esphome/components/pcm5122/pcm5122.h new file mode 100644 index 00000000..137f9fa0 --- /dev/null +++ b/esphome/components/pcm5122/pcm5122.h @@ -0,0 +1,33 @@ +#pragma once + +#include "esphome/components/audio_dac/audio_dac.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace pcm5122 { + +class PCM5122 : public audio_dac::AudioDac, public Component, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + bool set_mute_off() override; + bool set_mute_on() override; + bool set_volume(float volume) override; + + bool is_muted() override; + float volume() override; + + protected: + bool write_mute_(); + bool write_volume_(); + + float volume_{0}; +}; + +} +} \ No newline at end of file diff --git a/esphome/components/satellite1/__init__.py b/esphome/components/satellite1/__init__.py new file mode 100644 index 00000000..1808597a --- /dev/null +++ b/esphome/components/satellite1/__init__.py @@ -0,0 +1,106 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins + +from esphome.components.spi import SPIDevice, register_spi_device, spi_device_schema + +from esphome.const import ( + CONF_ID, + CONF_MODE, + CONF_PIN, + CONF_PORT, + CONF_OUTPUT, + CONF_INPUT, + CONF_INVERTED +) + +CONF_SATELLITE1 = "satellite1" +CONF_XMOS_RST_PIN = "xmos_rst_pin" +CONF_FLASH_SW_PIN = "flash_sw_pin" + +DEPENDENCIES = ["spi"] +CODEOWNERS = ["@gnumpi"] + +satellite1_ns = cg.esphome_ns.namespace("satellite1") +Satellite1 = satellite1_ns.class_("Satellite1", SPIDevice, cg.Component ) + +Satellite1SPIService = satellite1_ns.class_("Satellite1SPIService", cg.Parented.template(Satellite1) ) + +Satellite1GPIOPin = satellite1_ns.class_("Satellite1GPIOPin", cg.GPIOPin, Satellite1SPIService, cg.Parented.template(Satellite1) ) + + + + + +XMOSPort = satellite1_ns.enum("XMOSPort", is_class=True) +XMOS_PORT = { +"INPUT_A" : XMOSPort.INPUT_A, +"INPUT_B" : XMOSPort.INPUT_B, +"OUTPUT_A" : XMOSPort.OUTPUT_A +} + + +CONFIG_SCHEMA = ( + cv.Schema({ + cv.GenerateID(): cv.declare_id(Satellite1), + cv.Optional(CONF_XMOS_RST_PIN, default="GPIO12"): pins.gpio_output_pin_schema, + cv.Optional(CONF_FLASH_SW_PIN, default="GPIO14"): pins.gpio_output_pin_schema, + + }).extend(spi_device_schema(True, "1Hz")) +) + + +def validate_mode(value): + if not (value[CONF_INPUT] or value[CONF_OUTPUT]): + raise cv.Invalid("Mode must be either input or output") + if value[CONF_INPUT] and value[CONF_OUTPUT]: + raise cv.Invalid("Mode must be either input or output") + return value + + +PIN_SCHEMA = cv.All( + { + cv.GenerateID(): cv.declare_id(Satellite1GPIOPin), + cv.Required(CONF_SATELLITE1): cv.use_id(Satellite1), + cv.Required(CONF_PORT): cv.enum(XMOS_PORT), + cv.Required(CONF_PIN): cv.int_range(min=0, max=7), + cv.Optional(CONF_MODE, default=CONF_OUTPUT): cv.All( + { + cv.Optional(CONF_INPUT, default=False): cv.boolean, + cv.Optional(CONF_OUTPUT, default=False): cv.boolean, + }, + validate_mode, + ), + cv.Optional(CONF_INVERTED, default=False): cv.boolean, + } +) + + +# Satellite1 +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await register_spi_device(var, config) + + rst_pin = await cg.gpio_pin_expression(config[CONF_XMOS_RST_PIN]) + cg.add(var.set_xmos_rst_pin(rst_pin)) + sw_pin = await cg.gpio_pin_expression(config[CONF_FLASH_SW_PIN]) + cg.add(var.set_flash_sw_pin(sw_pin)) + return var + + +@pins.PIN_SCHEMA_REGISTRY.register(CONF_SATELLITE1, PIN_SCHEMA) +async def satellite1_pin_to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_parented(var, config[CONF_SATELLITE1]) + + cg.add(var.set_pin(config[CONF_PORT], config[CONF_PIN])) + cg.add(var.set_inverted(config[CONF_INVERTED])) + port_mode = { + CONF_INPUT : config[CONF_PORT] in ["INPUT_A","INPUT_B"], + CONF_OUTPUT: config[CONF_PORT] in ["OUTPUT_A"] + } + cg.add(var.set_flags(pins.gpio_flags_expr(port_mode))) + + + return var diff --git a/esphome/components/satellite1/audio_dac/__init__.py b/esphome/components/satellite1/audio_dac/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/esphome/components/satellite1/automation.h b/esphome/components/satellite1/automation.h new file mode 100644 index 00000000..e69de29b diff --git a/esphome/components/satellite1/light/__init__.py b/esphome/components/satellite1/light/__init__.py new file mode 100644 index 00000000..79c4f265 --- /dev/null +++ b/esphome/components/satellite1/light/__init__.py @@ -0,0 +1,29 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import light +from esphome.const import CONF_OUTPUT_ID + +from .. import ( + CONF_SATELLITE1, + satellite1_ns, + Satellite1, + Satellite1SPIService +) + +DEPENDENCIES = ["satellite1"] +CODEOWNERS = ["@gnumpi"] + +LedRing = satellite1_ns.class_("LEDRing", light.AddressableLight, Satellite1SPIService) + +CONFIG_SCHEMA = light.ADDRESSABLE_LIGHT_SCHEMA.extend( + { + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(LedRing), + cv.GenerateID(CONF_SATELLITE1): cv.use_id(Satellite1), + } +) + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) + await light.register_light(var, config) + await cg.register_component(var, config) + await cg.register_parented(var, config[CONF_SATELLITE1]) \ No newline at end of file diff --git a/esphome/components/satellite1/light/led_ring.cpp b/esphome/components/satellite1/light/led_ring.cpp new file mode 100644 index 00000000..62aefc19 --- /dev/null +++ b/esphome/components/satellite1/light/led_ring.cpp @@ -0,0 +1,58 @@ +#include "led_ring.h" + +namespace esphome { +namespace satellite1 { + +static const char *const TAG = "LED-Ring"; + + +void LEDRing::setup() { + ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + this->buffer_size_ = this->size() * 3; + this->buf_ = allocator.allocate(this->buffer_size_); + if (this->buf_ == nullptr) { + esph_log_e(TAG, "Failed to allocate buffer of size %u", this->buffer_size_); + this->mark_failed(); + return; + } + this->effect_data_ = allocator.allocate(this->size()); + if (this->effect_data_ == nullptr) { + esph_log_e(TAG, "Failed to allocate effect data of size %u", this->num_leds_); + this->mark_failed(); + return; + } + memset(this->buf_, 0, this->buffer_size_); +} + + +float LEDRing::get_setup_priority() const { return setup_priority::HARDWARE; } + + +void LEDRing::dump_config() { + esph_log_config(TAG, "Satellite1 LED-Ring:"); + esph_log_config(TAG, " LEDs: %d", this->num_leds_); +} + + +void LEDRing::write_state(light::LightState *state){ + if (this->is_failed()){ + return; + } + this->parent_->transfer(LED_RES_ID, CMD_WRITE_LED_RING_RAW, this->buf_, this->buffer_size_); +} + + +light::ESPColorView LEDRing::get_view_internal(int32_t index) const { + size_t pos = index * 3; + return { + this->buf_ + pos + 1, //r + this->buf_ + pos + 0, //g + this->buf_ + pos + 2, //b + 0, // no separate white channel + this->effect_data_ + index, + &this->correction_ + }; +} + +} +} diff --git a/esphome/components/satellite1/light/led_ring.h b/esphome/components/satellite1/light/led_ring.h new file mode 100644 index 00000000..b9bfbc4e --- /dev/null +++ b/esphome/components/satellite1/light/led_ring.h @@ -0,0 +1,52 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/log.h" +#include "esphome/components/light/addressable_light.h" + +#include "esphome/components/satellite1/satellite1.h" + +namespace esphome { +namespace satellite1 { + +const uint32_t NUMBER_OF_LEDS = 24; + +const uint32_t LED_RES_ID = 200; +const uint32_t CMD_WRITE_LED_RING_RAW = 0; + + +class LEDRing : public light::AddressableLight, public Satellite1SPIService { +public: + LEDRing() : num_leds_(NUMBER_OF_LEDS) {} + + int32_t size() const override { return this->num_leds_; } + + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + + light::LightTraits get_traits() override { + auto traits = light::LightTraits(); + traits.set_supported_color_modes({light::ColorMode::RGB}); + return traits; + } + + void write_state(light::LightState *state) override; + + void clear_effect_data() override { + for (int i = 0; i < this->size(); i++) + this->effect_data_[i] = 0; + } + +protected: + light::ESPColorView get_view_internal(int32_t index) const override; + +private: + size_t buffer_size_{}; + uint8_t *effect_data_{nullptr}; + uint8_t *buf_{nullptr}; + int32_t num_leds_; +}; + +} //namespace satellite1 +} //namespace esphome diff --git a/esphome/components/satellite1/media_player/__init__.py b/esphome/components/satellite1/media_player/__init__.py new file mode 100644 index 00000000..e77e90b4 --- /dev/null +++ b/esphome/components/satellite1/media_player/__init__.py @@ -0,0 +1,376 @@ +"""Nabu Media Player Setup.""" + +import hashlib +import logging +from pathlib import Path + +import esphome.codegen as cg +import esphome.config_validation as cv + +from esphome import automation, external_files, pins +from esphome.components import audio_dac, esp32, media_player +from esphome.components.media_player import MediaFile, MEDIA_FILE_TYPE_ENUM +from esphome.const import ( + CONF_DURATION, + CONF_FILE, + CONF_ID, + CONF_PATH, + CONF_RAW_DATA_ID, + CONF_TYPE, + CONF_URL, +) +from esphome.core import HexInt, CORE + +from esphome.components.i2s_audio import i2s_settings as i2s +from esphome.components.i2s_audio import ( + CONF_I2S_AUDIO_ID, + CONF_I2S_DOUT_PIN, + I2SAudioComponent, + I2SWriter, + i2s_audio_ns, + register_i2s_writer, +) + + +_LOGGER = logging.getLogger(__name__) + +try: + from esphome.external_files import download_content +except ImportError: + from esphome.components.font import download_content + +CODEOWNERS = ["@synesthesiam", "@kahrendt"] +DEPENDENCIES = ["media_player"] +DOMAIN = "file" + +TYPE_LOCAL = "local" +TYPE_WEB = "web" + +CONF_DECIBEL_REDUCTION = "decibel_reduction" + +CONF_AUDIO_DAC = "audio_dac" +CONF_MEDIA_FILE = "media_file" +CONF_FILES = "files" +CONF_SAMPLE_RATE = "sample_rate" +CONF_VOLUME_INCREMENT = "volume_increment" +CONF_VOLUME_MIN = "volume_min" +CONF_VOLUME_MAX = "volume_max" + +CONF_ON_MUTE = "on_mute" +CONF_ON_UNMUTE = "on_unmute" +CONF_ON_VOLUME = "on_volume" + +nabu_ns = cg.esphome_ns.namespace("nabu") +NabuMediaPlayer = nabu_ns.class_("NabuMediaPlayer") +NabuMediaPlayer = nabu_ns.class_( + "NabuMediaPlayer", + NabuMediaPlayer, + media_player.MediaPlayer, + cg.Component, + I2SWriter, +) + +DuckingSetAction = nabu_ns.class_( + "DuckingSetAction", automation.Action, cg.Parented.template(NabuMediaPlayer) +) +PlayLocalMediaAction = nabu_ns.class_( + "PlayLocalMediaAction", automation.Action, cg.Parented.template(NabuMediaPlayer) +) + + +def _compute_local_file_path(value: dict) -> Path: + url = value[CONF_URL] + h = hashlib.new("sha256") + h.update(url.encode()) + key = h.hexdigest()[:8] + base_dir = external_files.compute_local_file_dir(DOMAIN) + _LOGGER.debug("_compute_local_file_path: base_dir=%s", base_dir / key) + return base_dir / key + + +def _download_web_file(value): + url = value[CONF_URL] + path = _compute_local_file_path(value) + + download_content(url, path) + _LOGGER.debug("download_web_file: path=%s", path) + return value + + +LOCAL_SCHEMA = cv.Schema( + { + cv.Required(CONF_PATH): cv.file_, + } +) + +WEB_SCHEMA = cv.All( + { + cv.Required(CONF_URL): cv.url, + }, + _download_web_file, +) + + +def _validate_file_shorthand(value): + value = cv.string_strict(value) + if value.startswith("http://") or value.startswith("https://"): + return _file_schema( + { + CONF_TYPE: TYPE_WEB, + CONF_URL: value, + } + ) + return _file_schema( + { + CONF_TYPE: TYPE_LOCAL, + CONF_PATH: value, + } + ) + + +TYPED_FILE_SCHEMA = cv.typed_schema( + { + TYPE_LOCAL: LOCAL_SCHEMA, + TYPE_WEB: WEB_SCHEMA, + }, +) + + +def _file_schema(value): + if isinstance(value, str): + return _validate_file_shorthand(value) + return TYPED_FILE_SCHEMA(value) + + +def _validate_file_shorthand(value): + value = cv.string_strict(value) + if value.startswith("http://") or value.startswith("https://"): + return _file_schema( + { + CONF_TYPE: TYPE_WEB, + CONF_URL: value, + } + ) + return _file_schema( + { + CONF_TYPE: TYPE_LOCAL, + CONF_PATH: value, + } + ) + + +TYPED_FILE_SCHEMA = cv.typed_schema( + { + TYPE_LOCAL: LOCAL_SCHEMA, + TYPE_WEB: WEB_SCHEMA, + }, +) + + +def _file_schema(value): + if isinstance(value, str): + return _validate_file_shorthand(value) + return TYPED_FILE_SCHEMA(value) + + +MEDIA_FILE_TYPE_SCHEMA = cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(MediaFile), + cv.Required(CONF_FILE): _file_schema, + cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), + } +) + + +CONFIG_SCHEMA = media_player.MEDIA_PLAYER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(NabuMediaPlayer), + cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent), + cv.Optional(CONF_AUDIO_DAC): cv.use_id(audio_dac.AudioDac), + cv.Required(CONF_I2S_DOUT_PIN): pins.internal_gpio_output_pin_number, + cv.Optional(CONF_VOLUME_INCREMENT, default=0.05): cv.percentage, + cv.Optional(CONF_VOLUME_MAX, default=1.0): cv.percentage, + cv.Optional(CONF_VOLUME_MIN, default=0.0): cv.percentage, + cv.Optional(CONF_FILES): cv.ensure_list(MEDIA_FILE_TYPE_SCHEMA), + cv.Optional(CONF_ON_MUTE): automation.validate_automation(single=True), + cv.Optional(CONF_ON_UNMUTE): automation.validate_automation(single=True), + cv.Optional(CONF_ON_VOLUME): automation.validate_automation(single=True), + } +).extend( i2s.get_i2s_config_schema( + default_channel="right", default_rate=16000, default_bits="16bit" + )) + + +def _read_audio_file_and_type(file_config): + conf_file = file_config[CONF_FILE] + file_source = conf_file[CONF_TYPE] + if file_source == TYPE_LOCAL: + path = CORE.relative_config_path(conf_file[CONF_PATH]) + elif file_source == TYPE_WEB: + path = _compute_local_file_path(conf_file) + + with open(path, "rb") as f: + data = f.read() + + try: + import puremagic + + file_type: str = puremagic.from_string(data) + except ImportError: + try: + from magic import Magic + + magic = Magic(mime=True) + file_type: str = magic.from_buffer(data) + except ImportError as exc: + raise cv.Invalid("Please install puremagic") from exc + + if file_type.startswith("."): + file_type = file_type[1:] + elif file_type.startswith("audio/"): + file_type = file_type[6:] + + media_file_type = MEDIA_FILE_TYPE_ENUM["NONE"] + if "wav" in file_type: + media_file_type = MEDIA_FILE_TYPE_ENUM["WAV"] + elif file_type in ("mp3", "mpeg", "mpga"): + media_file_type = MEDIA_FILE_TYPE_ENUM["MP3"] + elif "flac" in file_type: + media_file_type = MEDIA_FILE_TYPE_ENUM["FLAC"] + + return data, media_file_type + + +def _supported_local_file_validate(config): + if files_list := config.get(CONF_FILES): + for file_config in files_list: + _, media_file_type = _read_audio_file_and_type(file_config) + if str(media_file_type) == str(MEDIA_FILE_TYPE_ENUM["NONE"]): + raise cv.Invalid("Unsupported local media file.") + + +FINAL_VALIDATE_SCHEMA = _supported_local_file_validate + + +async def to_code(config): + esp32.add_idf_component( + name="esp-dsp", + repo="https://github.com/kahrendt/esp-dsp", + ref="no-round-dot-product", + ) + + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await media_player.register_media_player(var, config) + + cg.add_define("USE_OTA_STATE_CALLBACK") + + await cg.register_parented(var, config[CONF_I2S_AUDIO_ID]) + await register_i2s_writer(var, config) + + cg.add(var.set_volume_increment(config[CONF_VOLUME_INCREMENT])) + cg.add(var.set_volume_max(config[CONF_VOLUME_MAX])) + cg.add(var.set_volume_min(config[CONF_VOLUME_MIN])) + + if on_mute := config.get(CONF_ON_MUTE): + await automation.build_automation( + var.get_mute_trigger(), + [], + on_mute, + ) + if on_unmute := config.get(CONF_ON_UNMUTE): + await automation.build_automation( + var.get_unmute_trigger(), + [], + on_unmute, + ) + if on_volume := config.get(CONF_ON_VOLUME): + await automation.build_automation( + var.get_volume_trigger(), + [(cg.float_, "x")], + on_volume, + ) + + if audio_dac_config := config.get(CONF_AUDIO_DAC): + aud_dac = await cg.get_variable(audio_dac_config) + cg.add(var.set_audio_dac(aud_dac)) + + if files_list := config.get(CONF_FILES): + for file_config in files_list: + data, media_file_type = _read_audio_file_and_type(file_config) + + rhs = [HexInt(x) for x in data] + prog_arr = cg.progmem_array(file_config[CONF_RAW_DATA_ID], rhs) + + media_files_struct = cg.StructInitializer( + MediaFile, + ( + "data", + prog_arr, + ), + ( + "length", + len(rhs), + ), + ( + "file_type", + media_file_type, + ), + ) + + cg.new_Pvariable( + file_config[CONF_ID], + media_files_struct, + ) + # decl = VariableDeclarationExpression(type, "*", name) + # CORE.add_global(decl) + # var = MockObj(name, "->") + # CORE.register_variable(name, var) + # return var + + # CORE.register_variable(MediaFile, file_config[CONF_ID]) + + +DUCKING_SET_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(NabuMediaPlayer), + cv.Required(CONF_DECIBEL_REDUCTION): cv.templatable( + cv.int_range(min=0, max=51) + ), + cv.Optional(CONF_DURATION, default="0.0s"): cv.templatable( + cv.positive_time_period_seconds + ), + } +) + + +# @automation.register_action( +# "nabu.play_local_media_file", +# PlayLocalMediaAction, +# cv.maybe_simple_value( +# { +# cv.GenerateID(): cv.use_id(NabuMediaPlayer), +# cv.Required(CONF_MEDIA_FILE): cv.use_id(MediaFile), +# }, +# key=CONF_MEDIA_FILE, +# ), +# ) +# async def media_player_play_media_action(config, action_id, template_arg, args): +# var = cg.new_Pvariable(action_id, template_arg) +# await cg.register_parented(var, config[CONF_ID]) +# media_file = config[CONF_MEDIA_FILE] +# cg.add(var.set_media_file(media_file)) +# return var + + +@automation.register_action("nabu.set_ducking", DuckingSetAction, DUCKING_SET_SCHEMA) +async def ducking_set_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + decibel_reduction = await cg.templatable( + config[CONF_DECIBEL_REDUCTION], args, cg.uint8 + ) + cg.add(var.set_decibel_reduction(decibel_reduction)) + duration = await cg.templatable(config[CONF_DURATION], args, cg.float_) + cg.add(var.set_duration(duration)) + return var diff --git a/esphome/components/satellite1/media_player/audio_decoder.cpp b/esphome/components/satellite1/media_player/audio_decoder.cpp new file mode 100644 index 00000000..a3c9dc08 --- /dev/null +++ b/esphome/components/satellite1/media_player/audio_decoder.cpp @@ -0,0 +1,404 @@ +#ifdef USE_ESP_IDF + +#include "audio_decoder.h" + +#include "mp3_decoder.h" + +#include "esphome/core/ring_buffer.h" + +#include "esphome/core/log.h" + +namespace esphome { +namespace nabu { + +static const char *const TAG = "nabu_media_player.decoder"; + +static const size_t READ_WRITE_TIMEOUT_MS = 20; + +AudioDecoder::AudioDecoder(RingBuffer *input_ring_buffer, RingBuffer *output_ring_buffer, size_t internal_buffer_size) { + this->input_ring_buffer_ = input_ring_buffer; + this->output_ring_buffer_ = output_ring_buffer; + this->internal_buffer_size_ = internal_buffer_size; +} + +AudioDecoder::~AudioDecoder() { + ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + if (this->input_buffer_ != nullptr) { + allocator.deallocate(this->input_buffer_, this->internal_buffer_size_); + } + if (this->output_buffer_ != nullptr) { + allocator.deallocate(this->output_buffer_, this->internal_buffer_size_); + } + + if (this->flac_decoder_ != nullptr) { + this->flac_decoder_->free_buffers(); + this->flac_decoder_.reset(); // Free the unique_ptr + this->flac_decoder_ = nullptr; + } + + if (this->media_file_type_ == media_player::MediaFileType::MP3) { + MP3FreeDecoder(this->mp3_decoder_); + } + + if (this->wav_decoder_ != nullptr) { + this->wav_decoder_.reset(); // Free the unique_ptr + this->wav_decoder_ = nullptr; + } +} + +esp_err_t AudioDecoder::start(media_player::MediaFileType media_file_type) { + esp_err_t err = this->allocate_buffers_(); + + if (err != ESP_OK) { + return err; + } + + ESP_LOGD(TAG, "Starting AudioDecoder."); + + this->media_file_type_ = media_file_type; + + this->input_buffer_current_ = this->input_buffer_; + this->input_buffer_length_ = 0; + this->output_buffer_current_ = this->output_buffer_; + this->output_buffer_length_ = 0; + + this->potentially_failed_count_ = 0; + this->end_of_file_ = false; + + switch (this->media_file_type_) { + case media_player::MediaFileType::FLAC: + this->flac_decoder_ = make_unique(this->input_buffer_); + break; + case media_player::MediaFileType::MP3: + this->mp3_decoder_ = MP3InitDecoder(); + break; + case media_player::MediaFileType::WAV: + this->wav_decoder_ = make_unique(&this->input_buffer_current_); + this->wav_decoder_->reset(); + break; + case media_player::MediaFileType::NONE: + return ESP_ERR_NOT_SUPPORTED; + break; + } + + return ESP_OK; +} + +AudioDecoderState AudioDecoder::decode(bool stop_gracefully) { + if (stop_gracefully) { + if (this->output_buffer_length_ == 0) { + // If the file decoder believes it the end of file + if (this->end_of_file_) { + return AudioDecoderState::FINISHED; + } + // If all the internal buffers are empty, the decoding is done + if ((this->input_ring_buffer_->available() == 0) && (this->input_buffer_length_ == 0)) { + return AudioDecoderState::FINISHED; + } + } + } + + if (this->potentially_failed_count_ > 10) { + ESP_LOGD(TAG, "Potentially failed more than 10 times."); + return AudioDecoderState::FAILED; + } + + FileDecoderState state = FileDecoderState::MORE_TO_PROCESS; + + while (state == FileDecoderState::MORE_TO_PROCESS) { + if (this->output_buffer_length_ > 0) { + // Have decoded data, write it to the output ring buffer + + size_t bytes_to_write = this->output_buffer_length_; + + if (bytes_to_write > 0) { + size_t bytes_written = this->output_ring_buffer_->write_without_replacement( + (void *) this->output_buffer_current_, bytes_to_write, pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS)); + + this->output_buffer_length_ -= bytes_written; + this->output_buffer_current_ += bytes_written; + } + + if (this->output_buffer_length_ > 0) { + // Output buffer still has decoded audio to write + return AudioDecoderState::DECODING; + } + } else { + // Decode more data + + // Shift unread data in input buffer to start + if (this->input_buffer_length_ > 0) { + memmove(this->input_buffer_, this->input_buffer_current_, this->input_buffer_length_); + } + this->input_buffer_current_ = this->input_buffer_; + + // read in new ring buffer data to fill the remaining input buffer + size_t bytes_read = 0; + + size_t bytes_to_read = this->internal_buffer_size_ - this->input_buffer_length_; + + if (bytes_to_read > 0) { + uint8_t *new_audio_data = this->input_buffer_ + this->input_buffer_length_; + bytes_read = this->input_ring_buffer_->read((void *) new_audio_data, bytes_to_read, pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS)); + + this->input_buffer_length_ += bytes_read; + } + + if ((this->input_buffer_length_ == 0) || ((this->potentially_failed_count_ > 0) && (bytes_read == 0))) { + ESP_LOGD(TAG, "skip decoding: buffer: %d, failed: %d, to-read: %d read: %d avail: %d", this->input_buffer_length_, + this->potentially_failed_count_, bytes_to_read, bytes_read, this->input_ring_buffer_->available() ); + if( (this->input_buffer_length_ && stop_gracefully) || bytes_to_read == 0 ){ + // data in buffer won't change, don't try again + state = FileDecoderState::FAILED; + } + else { + state = FileDecoderState::IDLE; + } + } else { + if(this->potentially_failed_count_ > 0){ + ESP_LOGD(TAG, "re-start decoding: buffer: %d, failed: %d, to-read: %d read: %d avail: %d", this->input_buffer_length_, + this->potentially_failed_count_, bytes_to_read, bytes_read, this->input_ring_buffer_->available() ); + } + + switch (this->media_file_type_) { + case media_player::MediaFileType::FLAC: + state = this->decode_flac_(); + break; + case media_player::MediaFileType::MP3: + state = this->decode_mp3_(); + break; + case media_player::MediaFileType::WAV: + state = this->decode_wav_(); + break; + case media_player::MediaFileType::NONE: + state = FileDecoderState::IDLE; + break; + } + } + } + if (state == FileDecoderState::POTENTIALLY_FAILED) { + ESP_LOGD(TAG, "Decoder potentially failed"); + ++this->potentially_failed_count_; + } else if (state == FileDecoderState::END_OF_FILE) { + this->end_of_file_ = true; + } else if (state == FileDecoderState::FAILED) { + return AudioDecoderState::FAILED; + } else if(state == FileDecoderState::MORE_TO_PROCESS){ + this->potentially_failed_count_ = 0; + } + } + return AudioDecoderState::DECODING; +} + +esp_err_t AudioDecoder::allocate_buffers_() { + ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + + if (this->input_buffer_ == nullptr) + this->input_buffer_ = allocator.allocate(this->internal_buffer_size_); + + if (this->output_buffer_ == nullptr) + this->output_buffer_ = allocator.allocate(this->internal_buffer_size_); + + if ((this->input_buffer_ == nullptr) || (this->output_buffer_ == nullptr)) { + return ESP_ERR_NO_MEM; + } + + return ESP_OK; +} + +FileDecoderState AudioDecoder::decode_flac_() { + if (!this->stream_info_.has_value()) { + // Header hasn't been read + auto result = this->flac_decoder_->read_header(this->input_buffer_length_); + + if (result == flac::FLAC_DECODER_HEADER_OUT_OF_DATA) { + ESP_LOGD(TAG, "FLAC OUT OF DATA"); + return FileDecoderState::POTENTIALLY_FAILED; + } + + if (result != flac::FLAC_DECODER_SUCCESS) { + // Couldn't read FLAC header + ESP_LOGD(TAG, "Couldn't read FLAC header"); + return FileDecoderState::FAILED; + } + + size_t bytes_consumed = this->flac_decoder_->get_bytes_index(); + this->input_buffer_current_ += bytes_consumed; + this->input_buffer_length_ = this->flac_decoder_->get_bytes_left(); + + if( this->flac_decoder_->get_num_channels() > 2 ){ + ESP_LOGE(TAG, "Decoder implementation only supports up to two channels."); + return FileDecoderState::FAILED; + } + + size_t flac_decoder_max_sample_bytes_in_frame = flac_decoder_->get_output_buffer_size_bytes(); + ESP_LOGV(TAG, "Block sizes min: %d, max: %d", flac_decoder_->get_min_block_size(), flac_decoder_->get_max_block_size()); + ESP_LOGV(TAG, "Maximal FLAC frame size (bytes): %d",flac_decoder_max_sample_bytes_in_frame ); + if (this->internal_buffer_size_ < flac_decoder_max_sample_bytes_in_frame ) { + // Output buffer is not big enough + ESP_LOGE(TAG, "Output buffer is not big enough"); + return FileDecoderState::FAILED; + } + + media_player::StreamInfo stream_info; + stream_info.channels = this->flac_decoder_->get_num_channels(); + stream_info.sample_rate = this->flac_decoder_->get_sample_rate(); + stream_info.bits_per_sample = this->flac_decoder_->get_sample_depth(); + + this->stream_info_ = stream_info; + + return FileDecoderState::MORE_TO_PROCESS; + } + + uint32_t output_samples = 0; + auto result = + this->flac_decoder_->decode_frame(this->input_buffer_length_, (int16_t *) this->output_buffer_, &output_samples); + + if (result == flac::FLAC_DECODER_ERROR_OUT_OF_DATA) { + ESP_LOGE(TAG, "FLAC_DECODER_ERROR_OUT_OF_DATA"); + return FileDecoderState::POTENTIALLY_FAILED; + } else if (result > flac::FLAC_DECODER_ERROR_OUT_OF_DATA) { + // Corrupted frame, don't retry with current buffer content, wait for new sync + ESP_LOGW(TAG, "Corrupted frame, error_code: %d. Re-syncing.", result ); + ESP_LOGD(TAG, "Bytes available in buffer: %d", this->input_buffer_length_ ); + ESP_LOGD(TAG, "Bytes read: %d", this->flac_decoder_->get_bytes_index() ); + ESP_LOGD(TAG, "Bytes left: %d", this->flac_decoder_->get_bytes_left() ); + + size_t bytes_consumed = this->flac_decoder_->get_bytes_index(); + this->input_buffer_current_ += bytes_consumed; + this->input_buffer_length_ = this->flac_decoder_->get_bytes_left(); + + return FileDecoderState::POTENTIALLY_FAILED; + } + // We have successfully decoded some input data and have new output data + size_t bytes_consumed = this->flac_decoder_->get_bytes_index(); + this->input_buffer_current_ += bytes_consumed; + this->input_buffer_length_ = this->flac_decoder_->get_bytes_left(); + + this->output_buffer_current_ = this->output_buffer_; + this->output_buffer_length_ = output_samples * sizeof(int16_t); + + if (result == flac::FLAC_DECODER_NO_MORE_FRAMES) { + return FileDecoderState::END_OF_FILE; + } + + return FileDecoderState::IDLE; +} + +FileDecoderState AudioDecoder::decode_mp3_() { + // Look for the next sync word + int32_t offset = MP3FindSyncWord(this->input_buffer_current_, this->input_buffer_length_); + if (offset < 0) { + // We may recover if we have more data + ESP_LOGD(TAG, "offset < 0" ); + return FileDecoderState::POTENTIALLY_FAILED; + } + + // Advance read pointer + this->input_buffer_current_ += offset; + this->input_buffer_length_ -= offset; + + int err = MP3Decode(this->mp3_decoder_, &this->input_buffer_current_, (int *) &this->input_buffer_length_, + (int16_t *) this->output_buffer_, 0); + if (err) { + switch (err) { + case ERR_MP3_MAINDATA_UNDERFLOW: + // Not a problem. Next call to decode will provide more data. + return FileDecoderState::POTENTIALLY_FAILED; + break; + default: + // TODO: Better handle mp3 decoder errors + return FileDecoderState::FAILED; + break; + } + } else { + MP3FrameInfo mp3_frame_info; + MP3GetLastFrameInfo(this->mp3_decoder_, &mp3_frame_info); + if (mp3_frame_info.outputSamps > 0) { + int bytes_per_sample = (mp3_frame_info.bitsPerSample / 8); + this->output_buffer_length_ = mp3_frame_info.outputSamps * bytes_per_sample; + this->output_buffer_current_ = this->output_buffer_; + + media_player::StreamInfo stream_info; + stream_info.channels = mp3_frame_info.nChans; + stream_info.sample_rate = mp3_frame_info.samprate; + stream_info.bits_per_sample = mp3_frame_info.bitsPerSample; + this->stream_info_ = stream_info; + } + } + + return FileDecoderState::MORE_TO_PROCESS; +} + +FileDecoderState AudioDecoder::decode_wav_() { + if (!this->stream_info_.has_value() && (this->input_buffer_length_ > 44)) { + // Header hasn't been processed + + size_t original_buffer_length = this->input_buffer_length_; + + size_t wav_bytes_to_skip = this->wav_decoder_->bytes_to_skip(); + size_t wav_bytes_to_read = this->wav_decoder_->bytes_needed(); + + bool header_finished = false; + while (!header_finished) { + if (wav_bytes_to_skip > 0) { + // Adjust pointer to skip the appropriate bytes + this->input_buffer_current_ += wav_bytes_to_skip; + this->input_buffer_length_ -= wav_bytes_to_skip; + wav_bytes_to_skip = 0; + } else if (wav_bytes_to_read > 0) { + wav_decoder::WAVDecoderResult result = this->wav_decoder_->next(); + this->input_buffer_current_ += wav_bytes_to_read; + this->input_buffer_length_ -= wav_bytes_to_read; + + if (result == wav_decoder::WAV_DECODER_SUCCESS_IN_DATA) { + // Header parsing is complete + + // Assume PCM + media_player::StreamInfo stream_info; + stream_info.channels = this->wav_decoder_->num_channels(); + stream_info.sample_rate = this->wav_decoder_->sample_rate(); + stream_info.bits_per_sample = this->wav_decoder_->bits_per_sample(); + this->stream_info_ = stream_info; + this->wav_bytes_left_ = this->wav_decoder_->chunk_bytes_left(); + header_finished = true; + } else if (result == wav_decoder::WAV_DECODER_SUCCESS_NEXT) { + // Continue parsing header + wav_bytes_to_skip = this->wav_decoder_->bytes_to_skip(); + wav_bytes_to_read = this->wav_decoder_->bytes_needed(); + } else { + // Unexpected error parsing the wav header + return FileDecoderState::FAILED; + } + } else { + // Something unexpected has happened + // Reset state and hope we have enough info next time + this->input_buffer_length_ = original_buffer_length; + this->input_buffer_current_ = this->input_buffer_; + return FileDecoderState::POTENTIALLY_FAILED; + } + } + } + + if (this->wav_bytes_left_ > 0) { + size_t bytes_to_write = std::min(this->wav_bytes_left_, this->input_buffer_length_); + bytes_to_write = std::min(bytes_to_write, this->internal_buffer_size_); + if (bytes_to_write > 0) { + std::memcpy(this->output_buffer_, this->input_buffer_current_, bytes_to_write); + this->input_buffer_current_ += bytes_to_write; + this->input_buffer_length_ -= bytes_to_write; + this->output_buffer_current_ = this->output_buffer_; + this->output_buffer_length_ = bytes_to_write; + this->wav_bytes_left_ -= bytes_to_write; + } + + return FileDecoderState::IDLE; + } + + return FileDecoderState::END_OF_FILE; +} + +} // namespace nabu +} // namespace esphome + +#endif \ No newline at end of file diff --git a/esphome/components/satellite1/media_player/audio_decoder.h b/esphome/components/satellite1/media_player/audio_decoder.h new file mode 100644 index 00000000..a44ae56b --- /dev/null +++ b/esphome/components/satellite1/media_player/audio_decoder.h @@ -0,0 +1,78 @@ +#pragma once + +#ifdef USE_ESP_IDF + +#include "flac_decoder.h" +#include "wav_decoder.h" +#include "mp3_decoder.h" + +#include "esphome/components/media_player/media_player.h" +#include "esphome/core/ring_buffer.h" + +namespace esphome { +namespace nabu { + +enum class AudioDecoderState : uint8_t { + INITIALIZED = 0, + DECODING, + FINISHED, + FAILED, +}; + +// Only used within the AudioDecoder class; conveys the state of the particular file type decoder +enum class FileDecoderState : uint8_t { + MORE_TO_PROCESS, + IDLE, + POTENTIALLY_FAILED, + FAILED, + END_OF_FILE, +}; + +class AudioDecoder { + public: + AudioDecoder(esphome::RingBuffer *input_ring_buffer, esphome::RingBuffer *output_ring_buffer, + size_t internal_buffer_size); + ~AudioDecoder(); + + esp_err_t start(media_player::MediaFileType media_file_type); + + AudioDecoderState decode(bool stop_gracefully); + + const optional &get_stream_info() const { return this->stream_info_; } + + protected: + esp_err_t allocate_buffers_(); + + FileDecoderState decode_flac_(); + FileDecoderState decode_mp3_(); + FileDecoderState decode_wav_(); + + esphome::RingBuffer *input_ring_buffer_; + esphome::RingBuffer *output_ring_buffer_; + size_t internal_buffer_size_; + + uint8_t *input_buffer_{nullptr}; + uint8_t *input_buffer_current_{nullptr}; + size_t input_buffer_length_; + + uint8_t *output_buffer_{nullptr}; + uint8_t *output_buffer_current_{nullptr}; + size_t output_buffer_length_; + + std::unique_ptr flac_decoder_; + + HMP3Decoder mp3_decoder_; + + std::unique_ptr wav_decoder_; + size_t wav_bytes_left_; + + media_player::MediaFileType media_file_type_{media_player::MediaFileType::NONE}; + optional stream_info_{}; + + size_t potentially_failed_count_{0}; + bool end_of_file_{false}; +}; +} // namespace nabu +} // namespace esphome + +#endif \ No newline at end of file diff --git a/esphome/components/satellite1/media_player/audio_mixer.cpp b/esphome/components/satellite1/media_player/audio_mixer.cpp new file mode 100644 index 00000000..570afac9 --- /dev/null +++ b/esphome/components/satellite1/media_player/audio_mixer.cpp @@ -0,0 +1,399 @@ +#ifdef USE_ESP_IDF + +#include "audio_mixer.h" + +#include "esp_dsp.h" + +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace nabu { + +static const size_t INPUT_RING_BUFFER_SAMPLES = 24000; // Audio samples +static const size_t OUTPUT_BUFFER_SAMPLES = 8192; // Audio samples - keep small for fast pausing +static const size_t QUEUE_COUNT = 20; + +static const uint32_t TASK_STACK_SIZE = 3072; +static const size_t TASK_DELAY_MS = 25; + +static const int16_t MAX_AUDIO_SAMPLE_VALUE = INT16_MAX; +static const int16_t MIN_AUDIO_SAMPLE_VALUE = INT16_MIN; + +esp_err_t AudioMixer::start(const std::string &task_name, UBaseType_t priority) { + esp_err_t err = this->allocate_buffers_(); + + if (err != ESP_OK) { + return err; + } + + if (this->task_handle_ == nullptr) { + this->task_handle_ = xTaskCreateStatic(AudioMixer::audio_mixer_task_, task_name.c_str(), TASK_STACK_SIZE, + (void *) this, priority, this->stack_buffer_, &this->task_stack_); + } + + if (this->task_handle_ == nullptr) { + return ESP_FAIL; + } + + return ESP_OK; +} + +void AudioMixer::stop() { + vTaskDelete(this->task_handle_); + this->task_handle_ = nullptr; + + xQueueReset(this->event_queue_); + xQueueReset(this->command_queue_); +} + +size_t AudioMixer::read(uint8_t *buffer, size_t length, TickType_t ticks_to_wait) { + return this->output_ring_buffer_->read((void *) buffer, length, ticks_to_wait); +} + +void AudioMixer::suspend_task() { + if (this->task_handle_ != nullptr) { + vTaskSuspend(this->task_handle_); + } +} + +void AudioMixer::resume_task() { + if (this->task_handle_ != nullptr) { + vTaskResume(task_handle_); + } +} + +void AudioMixer::audio_mixer_task_(void *params) { + AudioMixer *this_mixer = (AudioMixer *) params; + + TaskEvent event; + CommandEvent command_event; + + ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + int16_t *media_buffer = allocator.allocate(OUTPUT_BUFFER_SAMPLES); + int16_t *announcement_buffer = allocator.allocate(OUTPUT_BUFFER_SAMPLES); + int16_t *combination_buffer = allocator.allocate(OUTPUT_BUFFER_SAMPLES); + + int16_t *combination_buffer_current = combination_buffer; + size_t combination_buffer_length = 0; + + if ((media_buffer == nullptr) || (announcement_buffer == nullptr)) { + event.type = EventType::WARNING; + event.err = ESP_ERR_NO_MEM; + xQueueSend(this_mixer->event_queue_, &event, portMAX_DELAY); + + event.type = EventType::STOPPED; + event.err = ESP_OK; + xQueueSend(this_mixer->event_queue_, &event, portMAX_DELAY); + + while (true) { + delay(TASK_DELAY_MS); + } + + return; + } + + // Handles media stream pausing + bool transfer_media = true; + + // Parameters to control the ducking dB reduction and its transitions + // There is a built in negative sign; e.g., reducing by 5 dB is changing the gain by -5 dB + int8_t target_ducking_db_reduction = 0; + int8_t current_ducking_db_reduction = 0; + + // Each step represents a change in 1 dB. Positive 1 means the dB reduction is increasing. Negative 1 means the dB + // reduction is decreasing. + int8_t db_change_per_ducking_step = 1; + + size_t ducking_transition_samples_remaining = 0; + size_t samples_per_ducking_step = 0; + + event.type = EventType::STARTED; + xQueueSend(this_mixer->event_queue_, &event, portMAX_DELAY); + + while (true) { + if (xQueueReceive(this_mixer->command_queue_, &command_event, 0) == pdTRUE) { + if (command_event.command == CommandEventType::STOP) { + break; + } else if (command_event.command == CommandEventType::DUCK) { + if (target_ducking_db_reduction != command_event.decibel_reduction) { + current_ducking_db_reduction = target_ducking_db_reduction; + + target_ducking_db_reduction = command_event.decibel_reduction; + + uint8_t total_ducking_steps = 0; + if (target_ducking_db_reduction > current_ducking_db_reduction) { + // The dB reduction level is increasing (which results in quieter audio) + total_ducking_steps = target_ducking_db_reduction - current_ducking_db_reduction - 1; + db_change_per_ducking_step = 1; + } else { + // The dB reduction level is decreasing (which results in louder audio) + total_ducking_steps = current_ducking_db_reduction - target_ducking_db_reduction - 1; + db_change_per_ducking_step = -1; + } + if (total_ducking_steps > 0) { + ducking_transition_samples_remaining = command_event.transition_samples; + + samples_per_ducking_step = ducking_transition_samples_remaining / total_ducking_steps; + } else { + ducking_transition_samples_remaining = 0; + } + } + } else if (command_event.command == CommandEventType::PAUSE_MEDIA) { + transfer_media = false; + } else if (command_event.command == CommandEventType::RESUME_MEDIA) { + transfer_media = true; + } else if (command_event.command == CommandEventType::CLEAR_MEDIA) { + this_mixer->media_ring_buffer_->reset(); + } else if (command_event.command == CommandEventType::CLEAR_ANNOUNCEMENT) { + this_mixer->announcement_ring_buffer_->reset(); + } + } + + if (combination_buffer_length > 0) { + size_t output_bytes_written = this_mixer->output_ring_buffer_->write_without_replacement( + (void *) combination_buffer, combination_buffer_length, pdMS_TO_TICKS(TASK_DELAY_MS)); + combination_buffer_length -= output_bytes_written; + if ((combination_buffer_length > 0) && (output_bytes_written > 0)) { + memmove(combination_buffer, combination_buffer + output_bytes_written / sizeof(int16_t), + combination_buffer_length); + } + } else { + size_t media_available = this_mixer->media_ring_buffer_->available(); + size_t announcement_available = this_mixer->announcement_ring_buffer_->available(); + + if (media_available * transfer_media + announcement_available > 0) { + size_t bytes_to_read = OUTPUT_BUFFER_SAMPLES * sizeof(int16_t); + + if (media_available * transfer_media > 0) { + bytes_to_read = std::min(bytes_to_read, media_available); + } + + if (announcement_available > 0) { + bytes_to_read = std::min(bytes_to_read, announcement_available); + } + + if (bytes_to_read > 0) { + size_t media_bytes_read = 0; + if (media_available * transfer_media > 0) { + media_bytes_read = this_mixer->media_ring_buffer_->read((void *) media_buffer, bytes_to_read, 0); + if (media_bytes_read > 0) { + size_t samples_read = media_bytes_read / sizeof(int16_t); + if (ducking_transition_samples_remaining > 0) { + // Ducking level is still transitioning + + size_t samples_left = ducking_transition_samples_remaining; + + // There may be more than one step worth of samples to duck in the buffers, so manage positions + int16_t *current_media_buffer = media_buffer; + + size_t samples_left_in_step = samples_left % samples_per_ducking_step; + if (samples_left_in_step == 0) { + // Start of a new ducking step + + current_ducking_db_reduction += db_change_per_ducking_step; + samples_left_in_step = samples_per_ducking_step; + } + size_t samples_left_to_duck = std::min(samples_left_in_step, samples_read); + + size_t total_samples_ducked = 0; + + while (samples_left_to_duck > 0) { + // Ensure we only point to valid index in the Q15 scaling factor table + uint8_t safe_db_reduction_index = + clamp(current_ducking_db_reduction, 0, decibel_reduction_table.size() - 1); + + int16_t q15_scale_factor = decibel_reduction_table[safe_db_reduction_index]; + this_mixer->scale_audio_samples_(current_media_buffer, current_media_buffer, q15_scale_factor, + samples_left_to_duck); + + current_media_buffer += samples_left_to_duck; + + samples_read -= samples_left_to_duck; + samples_left -= samples_left_to_duck; + + total_samples_ducked += samples_left_to_duck; + + samples_left_in_step = samples_left % samples_per_ducking_step; + if (samples_left_in_step == 0) { + // Start of a new step + + current_ducking_db_reduction += db_change_per_ducking_step; + samples_left_in_step = samples_per_ducking_step; + } + samples_left_to_duck = std::min(samples_left_in_step, samples_read); + } + } else if (target_ducking_db_reduction > 0) { + // We still need to apply a ducking scaling, but we are done transitioning + + uint8_t safe_db_reduction_index = + clamp(target_ducking_db_reduction, 0, decibel_reduction_table.size() - 1); + + int16_t q15_scale_factor = decibel_reduction_table[safe_db_reduction_index]; + this_mixer->scale_audio_samples_(media_buffer, media_buffer, q15_scale_factor, samples_read); + } + } + } + + size_t announcement_bytes_read = 0; + if (announcement_available > 0) { + announcement_bytes_read = + this_mixer->announcement_ring_buffer_->read((void *) announcement_buffer, bytes_to_read, 0); + } + + if ((media_bytes_read > 0) && (announcement_bytes_read > 0)) { + // We have both a media and an announcement stream, so mix them together + + size_t samples_read = bytes_to_read / sizeof(int16_t); + + this_mixer->mix_audio_samples_without_clipping_(media_buffer, announcement_buffer, combination_buffer, + samples_read); + + combination_buffer_length = samples_read * sizeof(int16_t); + } else if (media_bytes_read > 0) { + memcpy(combination_buffer, media_buffer, media_bytes_read); + combination_buffer_length = media_bytes_read; + } else if (announcement_bytes_read > 0) { + memcpy(combination_buffer, announcement_buffer, announcement_bytes_read); + combination_buffer_length = announcement_bytes_read; + } + + size_t samples_written = combination_buffer_length / sizeof(int16_t); + if (ducking_transition_samples_remaining > 0) { + ducking_transition_samples_remaining -= std::min(samples_written, ducking_transition_samples_remaining); + } + } + } else { + // No audio data available in either buffer + + delay(TASK_DELAY_MS); + } + } + } + + event.type = EventType::STOPPING; + xQueueSend(this_mixer->event_queue_, &event, portMAX_DELAY); + + this_mixer->reset_ring_buffers_(); + allocator.deallocate(media_buffer, OUTPUT_BUFFER_SAMPLES); + allocator.deallocate(announcement_buffer, OUTPUT_BUFFER_SAMPLES); + allocator.deallocate(combination_buffer, OUTPUT_BUFFER_SAMPLES); + + event.type = EventType::STOPPED; + xQueueSend(this_mixer->event_queue_, &event, portMAX_DELAY); + + while (true) { + delay(TASK_DELAY_MS); + } +} + +esp_err_t AudioMixer::allocate_buffers_() { + if (this->media_ring_buffer_ == nullptr) + this->media_ring_buffer_ = RingBuffer::create(INPUT_RING_BUFFER_SAMPLES * sizeof(int16_t)); + + if (this->announcement_ring_buffer_ == nullptr) + this->announcement_ring_buffer_ = RingBuffer::create(INPUT_RING_BUFFER_SAMPLES * sizeof(int16_t)); + + if (this->output_ring_buffer_ == nullptr) + this->output_ring_buffer_ = RingBuffer::create(OUTPUT_BUFFER_SAMPLES * sizeof(int16_t)); + + if ((this->output_ring_buffer_ == nullptr) || (this->media_ring_buffer_ == nullptr) || + ((this->output_ring_buffer_ == nullptr))) { + return ESP_ERR_NO_MEM; + } + + if (this->stack_buffer_ == nullptr) + this->stack_buffer_ = (StackType_t *) malloc(TASK_STACK_SIZE); + + if (this->stack_buffer_ == nullptr) { + return ESP_ERR_NO_MEM; + } + + if (this->event_queue_ == nullptr) + this->event_queue_ = xQueueCreate(QUEUE_COUNT, sizeof(TaskEvent)); + + if (this->command_queue_ == nullptr) + this->command_queue_ = xQueueCreate(QUEUE_COUNT, sizeof(CommandEvent)); + + if ((this->event_queue_ == nullptr) || (this->command_queue_ == nullptr)) { + return ESP_ERR_NO_MEM; + } + + return ESP_OK; +} + +void AudioMixer::reset_ring_buffers_() { + this->output_ring_buffer_->reset(); + this->media_ring_buffer_->reset(); + this->announcement_ring_buffer_->reset(); +} + +void AudioMixer::mix_audio_samples_without_clipping_(int16_t *media_buffer, int16_t *announcement_buffer, + int16_t *combination_buffer, size_t samples_to_mix) { + // We first test adding the two clips samples together and check for any clipping + // We want the announcement volume to be consistent, regardless if media is playing or not + // If there is clipping, we determine what factor we need to multiply that media sample by to avoid it + // We take the smallest factor necessary for all the samples so the media volume is consistent on this batch + // of samples + // Note: This may not be the best approach. Adding 2 audio samples together makes both sound louder, even if + // we are not clipping. As a result, the mixed announcement will sound louder (by around 3dB if the audio + // streams are independent?) than if it were by itself. + + int16_t q15_scaling_factor = MAX_AUDIO_SAMPLE_VALUE; + + for (int i = 0; i < samples_to_mix; ++i) { + int32_t added_sample = static_cast(media_buffer[i]) + static_cast(announcement_buffer[i]); + + if ((added_sample > MAX_AUDIO_SAMPLE_VALUE) || (added_sample < MIN_AUDIO_SAMPLE_VALUE)) { + // The largest magnitude the media sample can be to avoid clipping (converted to Q30 fixed point) + int32_t q30_media_sample_safe_max = + static_cast(std::abs(MIN_AUDIO_SAMPLE_VALUE) - std::abs(announcement_buffer[i])) << 15; + + // Actual media sample value (Q15 number stored in an int32 for future division) + int32_t media_sample_value = abs(media_buffer[i]); + + // Calculation to perform the Q15 division for media_sample_safe_max/media_sample_value + // Reference: https://sestevenson.wordpress.com/2010/09/20/fixed-point-division-2/ (accessed August 15, + // 2024) + int16_t necessary_q15_factor = static_cast(q30_media_sample_safe_max / media_sample_value); + // Take the minimum scaling factor (the smaller the factor, the more it needs to be scaled down) + q15_scaling_factor = std::min(necessary_q15_factor, q15_scaling_factor); + } else { + // Store the combined samples in the combination buffer. If we do not need to scale, then the samples are already + // mixed. + combination_buffer[i] = added_sample; + } + } + + if (q15_scaling_factor < MAX_AUDIO_SAMPLE_VALUE) { + // Need to scale to avoid clipping + + this->scale_audio_samples_(media_buffer, media_buffer, q15_scaling_factor, samples_to_mix); + + // Mix both stream by adding them together with no bitshift + // The dsps_add functions have the following inputs: + // (buffer 1, buffer 2, output buffer, number of samples, buffer 1 step, buffer 2 step, output, buffer step, + // bitshift) +#if defined(USE_ESP32_VARIANT_ESP32S3) + dsps_add_s16_aes3(media_buffer, announcement_buffer, combination_buffer, samples_to_mix, 1, 1, 1, 0); +#elif defined(USE_ESP32_VARIANT_ESP32) + dsps_add_s16_ae32(media_buffer, announcement_buffer, combination_buffer, samples_to_mix, 1, 1, 1, 0); +#else + dsps_add_s16_ansi(media_buffer, announcement_buffer, combination_buffer, samples_to_mix, 1, 1, 1, 0); +#endif + } +} + +void AudioMixer::scale_audio_samples_(int16_t *audio_samples, int16_t *output_buffer, int16_t scale_factor, + size_t samples_to_scale) { + // Scale the audio samples and store them in the output buffer +#if defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32) + dsps_mulc_s16_ae32(audio_samples, output_buffer, samples_to_scale, scale_factor, 1, 1); +#else + dsps_mulc_s16_ansi(audio_samples, output_buffer, samples_to_scale, scale_factor, 1, 1); +#endif +} + +} // namespace nabu +} // namespace esphome +#endif \ No newline at end of file diff --git a/esphome/components/satellite1/media_player/audio_mixer.h b/esphome/components/satellite1/media_player/audio_mixer.h new file mode 100644 index 00000000..92706fc6 --- /dev/null +++ b/esphome/components/satellite1/media_player/audio_mixer.h @@ -0,0 +1,167 @@ +#pragma once + +#ifdef USE_ESP_IDF + +#include "esphome/components/media_player/media_player.h" + +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include "esphome/core/ring_buffer.h" + +#include +#include + +namespace esphome { +namespace nabu { + +// Mixes two incoming audio streams together +// - The media stream intended for music playback +// - Able to duck (made quieter) +// - Able to pause +// - The announcement stream is intended for TTS reponses or various beeps/sound effects +// - Unable to duck +// - Unable to pause +// - Each stream has a corresponding input ring buffer. Retrieved via the `get_media_ring_buffer` and +// `get_announcement_ring_buffer` functions +// - The mixed audio is stored in the output ring buffer. Use the `read` function to access +// - The mixer runs as a FreeRTOS task +// - The task reports its state using the TaskEvent queue. Regularly call the `read_event` function to obtain the +// current state +// - Commands are sent to the task using a the CommandEvent queue. Use the `send_command` function to do so. +// - Use the `start` function to initiate. The `stop` function deletes the task, but be sure to send a STOP command +// first to avoid memory leaks. + +enum class EventType : uint8_t { + STARTING = 0, + STARTED, + RUNNING, + IDLE, + STOPPING, + STOPPED, + WARNING = 255, +}; + +// Used for reporting the state of the mixer task +struct TaskEvent { + EventType type; + esp_err_t err; +}; + +enum class CommandEventType : uint8_t { + STOP, // Stop mixing to prepare for stopping the mixing task + DUCK, // Duck the media audio + PAUSE_MEDIA, // Pauses the media stream + RESUME_MEDIA, // Resumes the media stream + CLEAR_MEDIA, // Resets the media ring buffer + CLEAR_ANNOUNCEMENT, // Resets the announcement ring buffer +}; + +// Used to send commands to the mixer task +struct CommandEvent { + CommandEventType command; + uint8_t decibel_reduction; + size_t transition_samples = 0; +}; + +// Gives the Q15 fixed point scaling factor to reduce by 0 dB, 1dB, ..., 50 dB +// dB to PCM scaling factor formula: floating_point_scale_factor = 2^(-db/6.014) +// float to Q15 fixed point formula: q15_scale_factor = floating_point_scale_factor * 2^(15) +static const std::vector decibel_reduction_table = { + 32767, 29201, 26022, 23189, 20665, 18415, 16410, 14624, 13032, 11613, 10349, 9222, 8218, 7324, 6527, 5816, 5183, + 4619, 4116, 3668, 3269, 2913, 2596, 2313, 2061, 1837, 1637, 1459, 1300, 1158, 1032, 920, 820, 731, + 651, 580, 517, 461, 411, 366, 326, 291, 259, 231, 206, 183, 163, 146, 130, 116, 103}; + +class AudioMixer { + public: + /// @brief Returns the number of bytes available to read from the ring buffer + size_t available() { return this->output_ring_buffer_->available(); } + + /// @brief Reads from the output ring buffer + /// @param buffer stores the read data + /// @param length how many bytes requested to read from the ring buffer + /// @return number of bytes actually read; will be less than length if not available in ring buffer + size_t read(uint8_t *buffer, size_t length, TickType_t ticks_to_wait = 0); + + /// @brief Sends a CommandEvent to the command queue + /// @param command Pointer to CommandEvent object to be sent + /// @param ticks_to_wait The number of FreeRTOS ticks to wait for an event to appear on the queue. Defaults to 0. + /// @return pdTRUE if successful, pdFALSE otherwises + BaseType_t send_command(CommandEvent *command, TickType_t ticks_to_wait = portMAX_DELAY) { + return xQueueSend(this->command_queue_, command, ticks_to_wait); + } + + /// @brief Reads a TaskEvent from the event queue indicating its current status + /// @param event Pointer to TaskEvent object to store the event in + /// @param ticks_to_wait The number of FreeRTOS ticks to wait for an event to appear on the queue. Defaults to 0. + /// @return pdTRUE if successful, pdFALSE otherwise + BaseType_t read_event(TaskEvent *event, TickType_t ticks_to_wait = 0) { + return xQueueReceive(this->event_queue_, event, ticks_to_wait); + } + + /// @brief Starts the mixer task + /// @param task_name FreeRTOS task name + /// @param priority FreeRTOS task priority. Defaults to 1 + /// @return ESP_OK if successful, and error otherwise + esp_err_t start(const std::string &task_name, UBaseType_t priority = 1); + + /// @brief Stops the mixer task and clears the queues + void stop(); + + /// @brief Retrieves the media stream's ring buffer pointer + /// @return pointer to media ring buffer + RingBuffer *get_media_ring_buffer() { return this->media_ring_buffer_.get(); } + + /// @brief Retrieves the announcement stream's ring buffer pointer + /// @return pointer to announcement ring buffer + RingBuffer *get_announcement_ring_buffer() { return this->announcement_ring_buffer_.get(); } + + /// @brief Suspends the mixer task + void suspend_task(); + /// @brief Resumes the mixer task + void resume_task(); + + protected: + /// @brief Allocates the ring buffers, task stack, and queues + /// @return ESP_OK if successful or an error otherwise + esp_err_t allocate_buffers_(); + + /// @brief Resets teh output, media, and anouncement ring buffers + void reset_ring_buffers_(); + + /// @brief Mixes the media and announcement samples. If the resulting audio clips, the media samples are first scaled. + /// @param media_buffer buffer for media samples + /// @param announcement_buffer buffer for announcement samples + /// @param combination_buffer buffer for the mixed samples + /// @param samples_to_mix number of samples in the media and annoucnement buffers to mix together + void mix_audio_samples_without_clipping_(int16_t *media_buffer, int16_t *announcement_buffer, + int16_t *combination_buffer, size_t samples_to_mix); + + /// @brief Scales audio samples. Scales in place when audio_samples = output_buffer. + /// @param audio_samples PCM int16 audio samples + /// @param output_buffer Buffer to store the scaled samples + /// @param scale_factor Q15 fixed point scaling factor + /// @param samples_to_scale Number of samples to scale + void scale_audio_samples_(int16_t *audio_samples, int16_t *output_buffer, int16_t scale_factor, + size_t samples_to_scale); + + static void audio_mixer_task_(void *params); + TaskHandle_t task_handle_{nullptr}; + StaticTask_t task_stack_; + StackType_t *stack_buffer_{nullptr}; + + // Reports events from the mixer task + QueueHandle_t event_queue_; + + // Stores commands to send the mixer task + QueueHandle_t command_queue_; + + // Stores the mixed audio + std::unique_ptr output_ring_buffer_; + + std::unique_ptr media_ring_buffer_; + std::unique_ptr announcement_ring_buffer_; +}; +} // namespace nabu +} // namespace esphome + +#endif \ No newline at end of file diff --git a/esphome/components/satellite1/media_player/audio_pipeline.cpp b/esphome/components/satellite1/media_player/audio_pipeline.cpp new file mode 100644 index 00000000..1913edd5 --- /dev/null +++ b/esphome/components/satellite1/media_player/audio_pipeline.cpp @@ -0,0 +1,532 @@ +#ifdef USE_ESP_IDF + +#include "audio_pipeline.h" + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace nabu { + +static const size_t QUEUE_COUNT = 10; + +static const size_t FILE_BUFFER_SIZE = 32 * 1024; +static const size_t FILE_RING_BUFFER_SIZE = 4 * FILE_BUFFER_SIZE;//64 * 1024; +static const size_t BUFFER_SIZE_SAMPLES = 32768; +static const size_t BUFFER_SIZE_BYTES = BUFFER_SIZE_SAMPLES * sizeof(int16_t); + +static const uint32_t READER_TASK_STACK_SIZE = 4096; +static const uint32_t DECODER_TASK_STACK_SIZE = 3072; +static const uint32_t RESAMPLER_TASK_STACK_SIZE = 3072; + +static const size_t INFO_ERROR_QUEUE_COUNT = 5; + +static const char *const TAG = "nabu_media_player.pipeline"; + +enum EventGroupBits : uint32_t { + // The stop() function clears all unfinished bits + // MESSAGE_* bits are only set by their respective tasks + + // Stops all activity in the pipeline elements and set by stop() or by each task + PIPELINE_COMMAND_STOP = (1 << 0), + + // Read audio from an HTTP source; cleared by reader task and set by start(uri,...) + READER_COMMAND_INIT_HTTP = (1 << 4), + // Read audio from an audio file from the flash; cleared by reader task and set by start(media_file,...) + READER_COMMAND_INIT_FILE = (1 << 5), + + // Audio file type is read after checking it is supported; cleared by decoder task + READER_MESSAGE_LOADED_MEDIA_TYPE = (1 << 6), + // Reader is done (either through a failure or just end of the stream); cleared by reader task + READER_MESSAGE_FINISHED = (1 << 7), + // Error reading the file; cleared by get_state() + READER_MESSAGE_ERROR = (1 << 8), + + // Decoder has determined the stream information; cleared by resampler + DECODER_MESSAGE_LOADED_STREAM_INFO = (1 << 11), + // Decoder is done (either through a faiilure or the end of the stream); cleared by decoder task + DECODER_MESSAGE_FINISHED = (1 << 12), + // Error decoding the file; cleared by get_state() by decoder task + DECODER_MESSAGE_ERROR = (1 << 13), + + // Resampler is done (either through a failure or the end of the stream); cleared by resampler task + RESAMPLER_MESSAGE_FINISHED = (1 << 17), + // Error resampling the file; cleared by get_state() + RESAMPLER_MESSAGE_ERROR = (1 << 18), + + // Cleared by respective tasks + FINISHED_BITS = READER_MESSAGE_FINISHED | DECODER_MESSAGE_FINISHED | RESAMPLER_MESSAGE_FINISHED, + UNFINISHED_BITS = ~(FINISHED_BITS | 0xff000000), // Only 24 bits are valid for the event group, so make sure first 8 + // bits of uint32 are not set; cleared by stop() +}; + +AudioPipeline::AudioPipeline(AudioMixer *mixer, AudioPipelineType pipeline_type) { + this->mixer_ = mixer; + this->pipeline_type_ = pipeline_type; +} + +esp_err_t AudioPipeline::start(const std::string &uri, uint32_t target_sample_rate, const std::string &task_name, + UBaseType_t priority) { + esp_err_t err = this->common_start_(target_sample_rate, task_name, priority); + + if (err == ESP_OK) { + this->current_uri_ = uri; + xEventGroupSetBits(this->event_group_, READER_COMMAND_INIT_HTTP); + } + + return err; +} + +esp_err_t AudioPipeline::start(media_player::MediaFile *media_file, uint32_t target_sample_rate, + const std::string &task_name, UBaseType_t priority) { + esp_err_t err = this->common_start_(target_sample_rate, task_name, priority); + + if (err == ESP_OK) { + this->current_media_file_ = media_file; + xEventGroupSetBits(this->event_group_, READER_COMMAND_INIT_FILE); + } + + return err; +} + +esp_err_t AudioPipeline::allocate_buffers_() { + if (this->raw_file_ring_buffer_ == nullptr) + this->raw_file_ring_buffer_ = RingBuffer::create(FILE_RING_BUFFER_SIZE); + + if (this->decoded_ring_buffer_ == nullptr) + this->decoded_ring_buffer_ = RingBuffer::create(BUFFER_SIZE_BYTES); + + if ((this->raw_file_ring_buffer_ == nullptr) || (this->decoded_ring_buffer_ == nullptr)) { + return ESP_ERR_NO_MEM; + } + + if (this->read_task_stack_buffer_ == nullptr) + this->read_task_stack_buffer_ = (StackType_t *) malloc(READER_TASK_STACK_SIZE); + + if (this->decode_task_stack_buffer_ == nullptr) + this->decode_task_stack_buffer_ = (StackType_t *) malloc(DECODER_TASK_STACK_SIZE); + + if (this->resample_task_stack_buffer_ == nullptr) + this->resample_task_stack_buffer_ = (StackType_t *) malloc(RESAMPLER_TASK_STACK_SIZE); + + if ((this->read_task_stack_buffer_ == nullptr) || (this->decode_task_stack_buffer_ == nullptr) || + (this->resample_task_stack_buffer_ == nullptr)) { + return ESP_ERR_NO_MEM; + } + + if (this->event_group_ == nullptr) + this->event_group_ = xEventGroupCreate(); + + if (this->event_group_ == nullptr) { + return ESP_ERR_NO_MEM; + } + + if (this->info_error_queue_ == nullptr) + this->info_error_queue_ = xQueueCreate(INFO_ERROR_QUEUE_COUNT, sizeof(InfoErrorEvent)); + + if (this->info_error_queue_ == nullptr) + return ESP_ERR_NO_MEM; + + return ESP_OK; +} + +esp_err_t AudioPipeline::common_start_(uint32_t target_sample_rate, const std::string &task_name, + UBaseType_t priority) { + esp_err_t err = this->allocate_buffers_(); + if (err != ESP_OK) { + return err; + } + + if (this->read_task_handle_ == nullptr) { + this->read_task_handle_ = + xTaskCreateStatic(AudioPipeline::read_task_, (task_name + "_read").c_str(), READER_TASK_STACK_SIZE, + (void *) this, priority, this->read_task_stack_buffer_, &this->read_task_stack_); + } + if (this->decode_task_handle_ == nullptr) { + this->decode_task_handle_ = + xTaskCreateStatic(AudioPipeline::decode_task_, (task_name + "_decode").c_str(), DECODER_TASK_STACK_SIZE, + (void *) this, priority, this->decode_task_stack_buffer_, &this->decode_task_stack_); + } + if (this->resample_task_handle_ == nullptr) { + this->resample_task_handle_ = + xTaskCreateStatic(AudioPipeline::resample_task_, (task_name + "_resample").c_str(), RESAMPLER_TASK_STACK_SIZE, + (void *) this, priority, this->resample_task_stack_buffer_, &this->resample_task_stack_); + } + + if ((this->read_task_handle_ == nullptr) || (this->decode_task_handle_ == nullptr) || + (this->resample_task_handle_ == nullptr)) { + return ESP_FAIL; + } + + this->target_sample_rate_ = target_sample_rate; + + return this->stop(); +} + +AudioPipelineState AudioPipeline::get_state() { + InfoErrorEvent event; + if (this->info_error_queue_ != nullptr) { + while (xQueueReceive(this->info_error_queue_, &event, 0)) { + switch (event.source) { + case InfoErrorSource::READER: + if (event.err.has_value()) { + ESP_LOGE(TAG, "Media reader encountered an error: %s", esp_err_to_name(event.err.value())); + } else if (event.file_type.has_value()) { + ESP_LOGD(TAG, "Reading %s file type", + media_player::media_player_file_type_to_string(event.file_type.value())); + } + + break; + case InfoErrorSource::DECODER: + if (event.err.has_value()) { + ESP_LOGE(TAG, "Decoder encountered an error: %s", esp_err_to_name(event.err.value())); + } else if (event.stream_info.has_value()) { + ESP_LOGD(TAG, "Decoded audio has %d channels, %" PRId32 " Hz sample rate, and %d bits per sample", + event.stream_info.value().channels, event.stream_info.value().sample_rate, + event.stream_info.value().bits_per_sample); + } + break; + case InfoErrorSource::RESAMPLER: + if (event.err.has_value()) { + ESP_LOGE(TAG, "Resampler encountered an error: %s", esp_err_to_name(event.err.has_value())); + } else if (event.resample_info.has_value()) { + if (event.resample_info.value().resample) { + ESP_LOGD(TAG, "Converting the audio sample rate"); + } + if (event.resample_info.value().mono_to_stereo) { + ESP_LOGD(TAG, "Converting mono channel audio to stereo channel audio"); + } + } + break; + } + } + } + + EventBits_t event_bits = xEventGroupGetBits(this->event_group_); + if (!this->read_task_handle_ && !this->decode_task_handle_ && !this->resample_task_handle_) { + return AudioPipelineState::STOPPED; + } + + if ((event_bits & READER_MESSAGE_ERROR)) { + xEventGroupClearBits(this->event_group_, READER_MESSAGE_ERROR); + return AudioPipelineState::ERROR_READING; + } + + if ((event_bits & DECODER_MESSAGE_ERROR)) { + xEventGroupClearBits(this->event_group_, DECODER_MESSAGE_ERROR); + return AudioPipelineState::ERROR_DECODING; + } + + if ((event_bits & RESAMPLER_MESSAGE_ERROR)) { + xEventGroupClearBits(this->event_group_, RESAMPLER_MESSAGE_ERROR); + return AudioPipelineState::ERROR_RESAMPLING; + } + + if ((event_bits & READER_MESSAGE_FINISHED) && (event_bits & DECODER_MESSAGE_FINISHED) && + (event_bits & RESAMPLER_MESSAGE_FINISHED)) { + return AudioPipelineState::STOPPED; + } + + return AudioPipelineState::PLAYING; +} + +esp_err_t AudioPipeline::stop() { + xEventGroupSetBits(this->event_group_, PIPELINE_COMMAND_STOP); + + uint32_t event_group_bits = xEventGroupWaitBits(this->event_group_, + FINISHED_BITS, // Bit message to read + pdFALSE, // Clear the bits on exit + pdTRUE, // Wait for all the bits, + pdMS_TO_TICKS(300)); // Duration to block/wait + + if (!(event_group_bits & READER_MESSAGE_FINISHED)) { + // Reader failed to stop + xEventGroupSetBits(this->event_group_, EventGroupBits::READER_MESSAGE_ERROR); + } + if (!(event_group_bits & DECODER_MESSAGE_FINISHED)) { + // Decoder failed to stop + xEventGroupSetBits(this->event_group_, EventGroupBits::DECODER_MESSAGE_ERROR); + } + if (!(event_group_bits & RESAMPLER_MESSAGE_FINISHED)) { + // Resampler failed to stop + xEventGroupSetBits(this->event_group_, EventGroupBits::RESAMPLER_MESSAGE_ERROR); + } + + if ((event_group_bits & FINISHED_BITS) != FINISHED_BITS) { + // Not all bits were set, so it timed out + return ESP_ERR_TIMEOUT; + } + + // Clear the ring buffer in the mixer; avoids playing incorrect audio when starting a new file while paused + CommandEvent command_event; + if (this->pipeline_type_ == AudioPipelineType::MEDIA) { + command_event.command = CommandEventType::CLEAR_MEDIA; + } else { + command_event.command = CommandEventType::CLEAR_ANNOUNCEMENT; + } + this->mixer_->send_command(&command_event); + + xEventGroupClearBits(this->event_group_, UNFINISHED_BITS); + this->reset_ring_buffers(); + + return ESP_OK; +} + +void AudioPipeline::reset_ring_buffers() { + this->raw_file_ring_buffer_->reset(); + this->decoded_ring_buffer_->reset(); +} + +void AudioPipeline::suspend_tasks() { + if (this->read_task_handle_ != nullptr) { + vTaskSuspend(this->read_task_handle_); + } + if (this->decode_task_handle_ != nullptr) { + vTaskSuspend(this->decode_task_handle_); + } + if (this->resample_task_handle_ != nullptr) { + vTaskSuspend(this->resample_task_handle_); + } +} + +void AudioPipeline::resume_tasks() { + if (this->read_task_handle_ != nullptr) { + vTaskResume(this->read_task_handle_); + } + if (this->decode_task_handle_ != nullptr) { + vTaskResume(this->decode_task_handle_); + } + if (this->resample_task_handle_ != nullptr) { + vTaskResume(this->resample_task_handle_); + } +} + +void AudioPipeline::read_task_(void *params) { + AudioPipeline *this_pipeline = (AudioPipeline *) params; + + while (true) { + xEventGroupSetBits(this_pipeline->event_group_, EventGroupBits::READER_MESSAGE_FINISHED); + + // Wait until the pipeline notifies us the source of the media file + EventBits_t event_bits = + xEventGroupWaitBits(this_pipeline->event_group_, + READER_COMMAND_INIT_FILE | READER_COMMAND_INIT_HTTP, // Bit message to read + pdTRUE, // Clear the bit on exit + pdFALSE, // Wait for all the bits, + portMAX_DELAY); // Block indefinitely until bit is set + + xEventGroupClearBits(this_pipeline->event_group_, EventGroupBits::READER_MESSAGE_FINISHED); + + { + InfoErrorEvent event; + event.source = InfoErrorSource::READER; + esp_err_t err = ESP_OK; + + AudioReader reader = AudioReader(this_pipeline->raw_file_ring_buffer_.get(), FILE_BUFFER_SIZE); + + if (event_bits & READER_COMMAND_INIT_FILE) { + err = reader.start(this_pipeline->current_media_file_, this_pipeline->current_media_file_type_); + } else { + err = reader.start(this_pipeline->current_uri_, this_pipeline->current_media_file_type_); + } + if (err != ESP_OK) { + // Send specific error message + event.err = err; + xQueueSend(this_pipeline->info_error_queue_, &event, portMAX_DELAY); + + // Setting up the reader failed, stop the pipeline + xEventGroupSetBits(this_pipeline->event_group_, + EventGroupBits::READER_MESSAGE_ERROR | EventGroupBits::PIPELINE_COMMAND_STOP); + } else { + // Send the file type to the pipeline + event.file_type = this_pipeline->current_media_file_type_; + xQueueSend(this_pipeline->info_error_queue_, &event, portMAX_DELAY); + + TickType_t xStartTime = xTaskGetTickCount(); + + while (this_pipeline->raw_file_ring_buffer_.get()->available() < 2 * FILE_BUFFER_SIZE) { + // If the maximum wait time is exceeded, exit the loop + AudioReaderState reader_state = reader.read(); + + if (reader_state == AudioReaderState::FINISHED) { + break; + } else if (reader_state == AudioReaderState::FAILED) { + xEventGroupSetBits(this_pipeline->event_group_, + EventGroupBits::READER_MESSAGE_ERROR | EventGroupBits::PIPELINE_COMMAND_STOP); + break; + } + if ((xTaskGetTickCount() - xStartTime) >= pdMS_TO_TICKS(5000)) { + break; + } + + // Wait for a short delay before checking again + vTaskDelay(pdMS_TO_TICKS(10)); // Delay 10ms (can be adjusted) + } + + + // Inform the decoder that the media type is available + xEventGroupSetBits(this_pipeline->event_group_, EventGroupBits::READER_MESSAGE_LOADED_MEDIA_TYPE); + } + + while (true) { + event_bits = xEventGroupGetBits(this_pipeline->event_group_); + + if (event_bits & PIPELINE_COMMAND_STOP) { + break; + } + + AudioReaderState reader_state = reader.read(); + + if (reader_state == AudioReaderState::FINISHED) { + break; + } else if (reader_state == AudioReaderState::FAILED) { + xEventGroupSetBits(this_pipeline->event_group_, + EventGroupBits::READER_MESSAGE_ERROR | EventGroupBits::PIPELINE_COMMAND_STOP); + break; + } + } + } + } +} + +void AudioPipeline::decode_task_(void *params) { + AudioPipeline *this_pipeline = (AudioPipeline *) params; + + while (true) { + xEventGroupSetBits(this_pipeline->event_group_, EventGroupBits::DECODER_MESSAGE_FINISHED); + + // Wait until the reader notifies us that the media type is available + EventBits_t event_bits = xEventGroupWaitBits(this_pipeline->event_group_, + READER_MESSAGE_LOADED_MEDIA_TYPE, // Bit message to read + pdTRUE, // Clear the bit on exit + pdFALSE, // Wait for all the bits, + portMAX_DELAY); // Block indefinitely until bit is set + + xEventGroupClearBits(this_pipeline->event_group_, EventGroupBits::DECODER_MESSAGE_FINISHED); + + { + InfoErrorEvent event; + event.source = InfoErrorSource::DECODER; + ESP_LOGD(TAG, "Creating and starting new AudioDecoder"); + std::unique_ptr decoder = make_unique( + this_pipeline->raw_file_ring_buffer_.get(), this_pipeline->decoded_ring_buffer_.get(), FILE_BUFFER_SIZE); + esp_err_t err = decoder->start(this_pipeline->current_media_file_type_); + + if (err != ESP_OK) { + // Send specific error message + event.err = err; + xQueueSend(this_pipeline->info_error_queue_, &event, portMAX_DELAY); + + // Setting up the decoder failed, stop the pipeline + xEventGroupSetBits(this_pipeline->event_group_, + EventGroupBits::DECODER_MESSAGE_ERROR | EventGroupBits::PIPELINE_COMMAND_STOP); + } + + bool has_stream_info = false; + + while (true) { + event_bits = xEventGroupGetBits(this_pipeline->event_group_); + + if (event_bits & PIPELINE_COMMAND_STOP) { + break; + } + + // Stop gracefully if the reader has finished + AudioDecoderState decoder_state = decoder->decode(event_bits & READER_MESSAGE_FINISHED); + + if (decoder_state == AudioDecoderState::FINISHED) { + break; + } else if (decoder_state == AudioDecoderState::FAILED) { + xEventGroupSetBits(this_pipeline->event_group_, + EventGroupBits::DECODER_MESSAGE_ERROR | EventGroupBits::PIPELINE_COMMAND_STOP); + break; + } + + if (!has_stream_info && decoder->get_stream_info().has_value()) { + has_stream_info = true; + + this_pipeline->current_stream_info_ = decoder->get_stream_info().value(); + + // Send the stream information to the pipeline + event.stream_info = this_pipeline->current_stream_info_; + xQueueSend(this_pipeline->info_error_queue_, &event, portMAX_DELAY); + + // Inform the resampler that the stream information is available + xEventGroupSetBits(this_pipeline->event_group_, EventGroupBits::DECODER_MESSAGE_LOADED_STREAM_INFO); + } + } + } + } +} + +void AudioPipeline::resample_task_(void *params) { + AudioPipeline *this_pipeline = (AudioPipeline *) params; + + while (true) { + xEventGroupSetBits(this_pipeline->event_group_, EventGroupBits::RESAMPLER_MESSAGE_FINISHED); + + // Wait until the decoder notifies us that the stream information is available + EventBits_t event_bits = xEventGroupWaitBits(this_pipeline->event_group_, + DECODER_MESSAGE_LOADED_STREAM_INFO, // Bit message to read + pdTRUE, // Clear the bit on exit + pdFALSE, // Wait for all the bits, + portMAX_DELAY); // Block indefinitely until bit is set + + xEventGroupClearBits(this_pipeline->event_group_, EventGroupBits::RESAMPLER_MESSAGE_FINISHED); + + { + InfoErrorEvent event; + event.source = InfoErrorSource::RESAMPLER; + + RingBuffer *output_ring_buffer = nullptr; + + if (this_pipeline->pipeline_type_ == AudioPipelineType::MEDIA) { + output_ring_buffer = this_pipeline->mixer_->get_media_ring_buffer(); + } else { + output_ring_buffer = this_pipeline->mixer_->get_announcement_ring_buffer(); + } + + AudioResampler resampler = + AudioResampler(this_pipeline->decoded_ring_buffer_.get(), output_ring_buffer, BUFFER_SIZE_SAMPLES); + + esp_err_t err = resampler.start(this_pipeline->current_stream_info_, this_pipeline->target_sample_rate_, + this_pipeline->current_resample_info_); + + if (err != ESP_OK) { + // Send specific error message + event.err = err; + xQueueSend(this_pipeline->info_error_queue_, &event, portMAX_DELAY); + + // Setting up the resampler failed, stop the pipeline + xEventGroupSetBits(this_pipeline->event_group_, + EventGroupBits::RESAMPLER_MESSAGE_ERROR | EventGroupBits::PIPELINE_COMMAND_STOP); + } else { + event.resample_info = this_pipeline->current_resample_info_; + xQueueSend(this_pipeline->info_error_queue_, &event, portMAX_DELAY); + } + + while (true) { + event_bits = xEventGroupGetBits(this_pipeline->event_group_); + + if (event_bits & PIPELINE_COMMAND_STOP) { + break; + } + + // Stop gracefully if the decoder is done + AudioResamplerState resampler_state = resampler.resample(event_bits & DECODER_MESSAGE_FINISHED); + + if (resampler_state == AudioResamplerState::FINISHED) { + break; + } else if (resampler_state == AudioResamplerState::FAILED) { + xEventGroupSetBits(this_pipeline->event_group_, + EventGroupBits::RESAMPLER_MESSAGE_ERROR | EventGroupBits::PIPELINE_COMMAND_STOP); + break; + } + } + } + } +} + +} // namespace nabu +} // namespace esphome +#endif \ No newline at end of file diff --git a/esphome/components/satellite1/media_player/audio_pipeline.h b/esphome/components/satellite1/media_player/audio_pipeline.h new file mode 100644 index 00000000..be048612 --- /dev/null +++ b/esphome/components/satellite1/media_player/audio_pipeline.h @@ -0,0 +1,145 @@ +#pragma once + +#ifdef USE_ESP_IDF + +#include "audio_reader.h" +#include "audio_decoder.h" +#include "audio_resampler.h" +#include "audio_mixer.h" + +#include "esphome/components/media_player/media_player.h" + +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include "esphome/core/ring_buffer.h" + +#include +#include +#include + +namespace esphome { +namespace nabu { + +enum class AudioPipelineType : uint8_t { + MEDIA, + ANNOUNCEMENT, +}; + +enum class AudioPipelineState : uint8_t { + PLAYING, + STOPPED, + ERROR_READING, + ERROR_DECODING, + ERROR_RESAMPLING, +}; + +enum class InfoErrorSource : uint8_t { + READER = 0, + DECODER, + RESAMPLER, +}; + +// Used to pass information from each task. +struct InfoErrorEvent { + InfoErrorSource source; + optional err; + optional file_type; + optional stream_info; + optional resample_info; +}; + +class AudioPipeline { + public: + AudioPipeline(AudioMixer *mixer, AudioPipelineType pipeline_type); + + /// @brief Starts an audio pipeline given a media url + /// @param uri media file url + /// @param target_sample_rate the desired sample rate of the audio stream + /// @param task_name FreeRTOS task name + /// @param priority FreeRTOS task priority + /// @return ESP_OK if successful or an appropriate error if not + esp_err_t start(const std::string &uri, uint32_t target_sample_rate, const std::string &task_name, + UBaseType_t priority = 1); + + /// @brief Starts an audio pipeline given a MediaFile pointer + /// @param media_file pointer to a MediaFile object + /// @param target_sample_rate the desired sample rate of the audio stream + /// @param task_name FreeRTOS task name + /// @param priority FreeRTOS task priority + /// @return ESP_OK if successful or an appropriate error if not + esp_err_t start(media_player::MediaFile *media_file, uint32_t target_sample_rate, const std::string &task_name, + UBaseType_t priority = 1); + + /// @brief Stops the pipeline. Sends a stop signal to each task (if running) and clears the ring buffers. + /// @return ESP_OK if successful or ESP_ERR_TIMEOUT if the tasks did not indicate they stopped + esp_err_t stop(); + + /// @brief Gets the state of the audio pipeline based on the info_error_queue_ and event_group_ + /// @return AudioPipelineState + AudioPipelineState get_state(); + + /// @brief Resets the ring buffers, discarding any existing data + void reset_ring_buffers(); + + /// @brief Suspends any running tasks + void suspend_tasks(); + /// @brief Resumes any running tasks + void resume_tasks(); + + protected: + /// @brief Allocates the ring buffers, event group, and info error queue. + /// @return ESP_OK if successful or ESP_ERR_NO_MEM if it is unable to allocate all parts + esp_err_t allocate_buffers_(); + + /// @brief Common start code for the pipeline, regardless if the source is a file or url. + /// @param target_sample_rate the desired sample rate of the audio stream + /// @param task_name FreeRTOS task name + /// @param priority FreeRTOS task priority + /// @return ESP_OK if successful or an appropriate error if not + esp_err_t common_start_(uint32_t target_sample_rate, const std::string &task_name, UBaseType_t priority); + + // Pointer to the media player's mixer object. The resample task feeds the appropriate ring buffer directly + AudioMixer *mixer_; + + std::string current_uri_{}; + media_player::MediaFile *current_media_file_{nullptr}; + + media_player::MediaFileType current_media_file_type_; + media_player::StreamInfo current_stream_info_; + ResampleInfo current_resample_info_; + uint32_t target_sample_rate_; + + AudioPipelineType pipeline_type_; + + std::unique_ptr raw_file_ring_buffer_; + std::unique_ptr decoded_ring_buffer_; + + // Handles basic control/state of the three tasks + EventGroupHandle_t event_group_{nullptr}; + + // Receives detailed info (file type, stream info, resampling info) or specific errors from the three tasks + QueueHandle_t info_error_queue_{nullptr}; + + // Handles reading the media file from flash or a url + static void read_task_(void *params); + TaskHandle_t read_task_handle_{nullptr}; + StaticTask_t read_task_stack_; + StackType_t *read_task_stack_buffer_{nullptr}; + + // Decodes the media file into PCM audio + static void decode_task_(void *params); + TaskHandle_t decode_task_handle_{nullptr}; + StaticTask_t decode_task_stack_; + StackType_t *decode_task_stack_buffer_{nullptr}; + + // Resamples the audio to match the specified target sample rate. Converts mono audio to stereo audio if necessary. + static void resample_task_(void *params); + TaskHandle_t resample_task_handle_{nullptr}; + StaticTask_t resample_task_stack_; + StackType_t *resample_task_stack_buffer_{nullptr}; +}; + +} // namespace nabu +} // namespace esphome + +#endif \ No newline at end of file diff --git a/esphome/components/satellite1/media_player/audio_reader.cpp b/esphome/components/satellite1/media_player/audio_reader.cpp new file mode 100644 index 00000000..a3e4fadf --- /dev/null +++ b/esphome/components/satellite1/media_player/audio_reader.cpp @@ -0,0 +1,215 @@ +#ifdef USE_ESP_IDF + +#include "audio_reader.h" + +#include "esphome/core/ring_buffer.h" + +#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE +#include "esp_crt_bundle.h" +#endif + +#include "esphome/core/log.h" + +namespace esphome { +namespace nabu { + +static const char *const TAG = "nabu_media_player.audio_reader"; + +static const size_t READ_WRITE_TIMEOUT_MS = 20; + +// The number of times the http read times out with no data before throwing an error +static const size_t ERROR_COUNT_NO_DATA_READ_TIMEOUT = 10; + +AudioReader::AudioReader(esphome::RingBuffer *output_ring_buffer, size_t transfer_buffer_size) { + this->output_ring_buffer_ = output_ring_buffer; + this->transfer_buffer_size_ = transfer_buffer_size; +} + +AudioReader::~AudioReader() { + if (this->transfer_buffer_ != nullptr) { + ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + allocator.deallocate(this->transfer_buffer_, this->transfer_buffer_size_); + } + + this->cleanup_connection_(); +} + +esp_err_t AudioReader::allocate_buffers_() { + ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + if (this->transfer_buffer_ == nullptr) + this->transfer_buffer_ = allocator.allocate(this->transfer_buffer_size_); + + if (this->transfer_buffer_ == nullptr) + return ESP_ERR_NO_MEM; + + return ESP_OK; +} + +esp_err_t AudioReader::start(media_player::MediaFile *media_file, media_player::MediaFileType &file_type) { + file_type = media_player::MediaFileType::NONE; + + esp_err_t err = this->allocate_buffers_(); + if (err != ESP_OK) { + return err; + } + + this->current_media_file_ = media_file; + + this->transfer_buffer_current_ = media_file->data; + this->transfer_buffer_length_ = media_file->length; + file_type = media_file->file_type; + + return ESP_OK; +} + +esp_err_t AudioReader::start(const std::string &uri, media_player::MediaFileType &file_type) { + file_type = media_player::MediaFileType::NONE; + + esp_err_t err = this->allocate_buffers_(); + if (err != ESP_OK) { + return err; + } + + this->cleanup_connection_(); + + if (uri.empty()) { + return ESP_ERR_INVALID_ARG; + } + + esp_http_client_config_t client_config = {}; + + client_config.url = uri.c_str(); + client_config.cert_pem = nullptr; + client_config.disable_auto_redirect = false; + client_config.max_redirection_count = 10; + client_config.buffer_size = 2048; + client_config.keep_alive_enable = true; + client_config.timeout_ms = 5000; // Doesn't raise an error if exceeded in esp-idf v4.4, it just prevents the + // http_client_read command from blocking for too long + +#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE + if (uri.find("https:") != std::string::npos) { + client_config.crt_bundle_attach = esp_crt_bundle_attach; + } +#endif + + this->client_ = esp_http_client_init(&client_config); + + if (this->client_ == nullptr) { + return ESP_FAIL; + } + + if ((err = esp_http_client_open(this->client_, 0)) != ESP_OK) { + this->cleanup_connection_(); + return err; + } + + int content_length = esp_http_client_fetch_headers(this->client_); + + char url[500]; + err = esp_http_client_get_url(this->client_, url, 500); + if (err != ESP_OK) { + this->cleanup_connection_(); + return err; + } + + std::string url_string = url; + + if (str_endswith(url_string, ".wav")) { + file_type = media_player::MediaFileType::WAV; + } else if (str_endswith(url_string, ".mp3")) { + file_type = media_player::MediaFileType::MP3; + } else if (str_endswith(url_string, ".flac")) { + file_type = media_player::MediaFileType::FLAC; + } else { + file_type = media_player::MediaFileType::NONE; + this->cleanup_connection_(); + return ESP_ERR_NOT_SUPPORTED; + } + + this->transfer_buffer_current_ = this->transfer_buffer_; + this->transfer_buffer_length_ = 0; + this->no_data_read_count_ = 0; + + return ESP_OK; +} + +AudioReaderState AudioReader::read() { + if (this->client_ != nullptr) { + return this->http_read_(); + } else if (this->current_media_file_ != nullptr) { + return this->file_read_(); + } + + return AudioReaderState::FAILED; +} + +AudioReaderState AudioReader::file_read_() { + if (this->transfer_buffer_length_ > 0) { + size_t bytes_written = this->output_ring_buffer_->write_without_replacement( + (void *) this->transfer_buffer_current_, this->transfer_buffer_length_, pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS)); + this->transfer_buffer_length_ -= bytes_written; + this->transfer_buffer_current_ += bytes_written; + + return AudioReaderState::READING; + } + return AudioReaderState::FINISHED; +} + +AudioReaderState AudioReader::http_read_() { + if (this->transfer_buffer_length_ > 0) { + size_t bytes_written = this->output_ring_buffer_->write_without_replacement( + (void *) this->transfer_buffer_, this->transfer_buffer_length_, pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS)); + this->transfer_buffer_length_ -= bytes_written; + + // Shift remaining data to the start of the transfer buffer + memmove(this->transfer_buffer_, this->transfer_buffer_ + bytes_written, this->transfer_buffer_length_); + } + + if (esp_http_client_is_complete_data_received(this->client_)) { + if (this->transfer_buffer_length_ == 0) { + this->cleanup_connection_(); + return AudioReaderState::FINISHED; + } + } else { + size_t bytes_to_read = this->transfer_buffer_size_ - this->transfer_buffer_length_; + int received_len = esp_http_client_read( + this->client_, (char *) this->transfer_buffer_ + this->transfer_buffer_length_, bytes_to_read); + + if (received_len > 0) { + this->transfer_buffer_length_ += received_len; + this->no_data_read_count_ = 0; + } else if (received_len < 0) { + // HTTP read error + this->cleanup_connection_(); + ESP_LOGE(TAG, "HTTP Error %d", received_len ); + return AudioReaderState::FAILED; + } else { + if (bytes_to_read > 0) { + // Read timed out + ++this->no_data_read_count_; + if (this->no_data_read_count_ >= ERROR_COUNT_NO_DATA_READ_TIMEOUT) { + // Timed out with no data read too many times, so the http read has failed + this->cleanup_connection_(); + ESP_LOGE(TAG, "Http read timed out."); + return AudioReaderState::FAILED; + } + } + } + } + + return AudioReaderState::READING; +} + +void AudioReader::cleanup_connection_() { + if (this->client_ != nullptr) { + esp_http_client_close(this->client_); + esp_http_client_cleanup(this->client_); + this->client_ = nullptr; + } +} + +} // namespace nabu +} // namespace esphome + +#endif \ No newline at end of file diff --git a/esphome/components/satellite1/media_player/audio_reader.h b/esphome/components/satellite1/media_player/audio_reader.h new file mode 100644 index 00000000..cbfea009 --- /dev/null +++ b/esphome/components/satellite1/media_player/audio_reader.h @@ -0,0 +1,54 @@ +#pragma once + +#ifdef USE_ESP_IDF + +#include "esphome/components/media_player/media_player.h" +#include "esphome/core/ring_buffer.h" + +#include + +namespace esphome { +namespace nabu { + +enum class AudioReaderState : uint8_t { + READING = 0, + FINISHED, + FAILED, +}; + +class AudioReader { + public: + AudioReader(esphome::RingBuffer *output_ring_buffer, size_t transfer_buffer_size); + ~AudioReader(); + + esp_err_t start(const std::string &uri, media_player::MediaFileType &file_type); + esp_err_t start(media_player::MediaFile *media_file, media_player::MediaFileType &file_type); + + AudioReaderState read(); + + protected: + esp_err_t allocate_buffers_(); + + AudioReaderState file_read_(); + AudioReaderState http_read_(); + + void cleanup_connection_(); + + esphome::RingBuffer *output_ring_buffer_; + + size_t transfer_buffer_length_; // Amount of data currently stored in transfer buffer (in bytes) + size_t transfer_buffer_size_; // Capacity of transfer buffer (in bytes) + + ssize_t no_data_read_count_; + + uint8_t *transfer_buffer_{nullptr}; + const uint8_t *transfer_buffer_current_{nullptr}; + + esp_http_client_handle_t client_{nullptr}; + + media_player::MediaFile *current_media_file_{nullptr}; +}; +} // namespace nabu +} // namespace esphome + +#endif \ No newline at end of file diff --git a/esphome/components/satellite1/media_player/audio_resampler.cpp b/esphome/components/satellite1/media_player/audio_resampler.cpp new file mode 100644 index 00000000..c6e01672 --- /dev/null +++ b/esphome/components/satellite1/media_player/audio_resampler.cpp @@ -0,0 +1,316 @@ +#ifdef USE_ESP_IDF + +#include "audio_resampler.h" + +#include "esphome/core/ring_buffer.h" + +namespace esphome { +namespace nabu { + +static const size_t NUM_TAPS = 32; +static const size_t NUM_FILTERS = 32; +static const bool USE_PRE_POST_FILTER = true; + +// These output parameters are currently hardcoded in the elements further down the pipeline (mixer and speaker) +static const uint8_t OUTPUT_CHANNELS = 2; +static const uint8_t OUTPUT_BITS_PER_SAMPLE = 16; + +static const size_t READ_WRITE_TIMEOUT_MS = 20; + +AudioResampler::AudioResampler(RingBuffer *input_ring_buffer, RingBuffer *output_ring_buffer, + size_t internal_buffer_samples) { + this->input_ring_buffer_ = input_ring_buffer; + this->output_ring_buffer_ = output_ring_buffer; + this->internal_buffer_samples_ = internal_buffer_samples; +} + +AudioResampler::~AudioResampler() { + ExternalRAMAllocator int16_allocator(ExternalRAMAllocator::ALLOW_FAILURE); + ExternalRAMAllocator float_allocator(ExternalRAMAllocator::ALLOW_FAILURE); + + if (this->input_buffer_ != nullptr) { + int16_allocator.deallocate(this->input_buffer_, this->internal_buffer_samples_); + } + if (this->output_buffer_ != nullptr) { + int16_allocator.deallocate(this->output_buffer_, this->internal_buffer_samples_); + } + if (this->float_input_buffer_ != nullptr) { + float_allocator.deallocate(this->float_input_buffer_, this->internal_buffer_samples_); + } + if (this->float_output_buffer_ != nullptr) { + float_allocator.deallocate(this->float_output_buffer_, this->internal_buffer_samples_); + } + if (this->resampler_ != nullptr) { + resampleFree(this->resampler_); + this->resampler_ = nullptr; + } +} + +esp_err_t AudioResampler::allocate_buffers_() { + ExternalRAMAllocator int16_allocator(ExternalRAMAllocator::ALLOW_FAILURE); + ExternalRAMAllocator float_allocator(ExternalRAMAllocator::ALLOW_FAILURE); + + if (this->input_buffer_ == nullptr) + this->input_buffer_ = int16_allocator.allocate(this->internal_buffer_samples_); + if (this->output_buffer_ == nullptr) + this->output_buffer_ = int16_allocator.allocate(this->internal_buffer_samples_); + + if (this->float_input_buffer_ == nullptr) + this->float_input_buffer_ = float_allocator.allocate(this->internal_buffer_samples_); + + if (this->float_output_buffer_ == nullptr) + this->float_output_buffer_ = float_allocator.allocate(this->internal_buffer_samples_); + + if ((this->input_buffer_ == nullptr) || (this->output_buffer_ == nullptr) || (this->float_input_buffer_ == nullptr) || + (this->float_output_buffer_ == nullptr)) { + return ESP_ERR_NO_MEM; + } + + return ESP_OK; +} + +esp_err_t AudioResampler::start(media_player::StreamInfo &stream_info, uint32_t target_sample_rate, + ResampleInfo &resample_info) { + esp_err_t err = this->allocate_buffers_(); + if (err != ESP_OK) { + return err; + } + + this->stream_info_ = stream_info; + + this->input_buffer_current_ = this->input_buffer_; + this->input_buffer_length_ = 0; + this->float_input_buffer_current_ = this->float_input_buffer_; + this->float_input_buffer_length_ = 0; + + this->output_buffer_current_ = this->output_buffer_; + this->output_buffer_length_ = 0; + this->float_output_buffer_current_ = this->float_output_buffer_; + this->float_output_buffer_length_ = 0; + + resample_info.mono_to_stereo = (stream_info.channels != 2); + + if ((stream_info.channels > OUTPUT_CHANNELS) || (stream_info_.bits_per_sample != OUTPUT_BITS_PER_SAMPLE)) { + return ESP_ERR_NOT_SUPPORTED; + } + + if (stream_info.channels > 0) { + this->channel_factor_ = 2 / stream_info.channels; + } + + if (stream_info.sample_rate != target_sample_rate) { + int flags = 0; + + resample_info.resample = true; + + this->sample_ratio_ = static_cast(target_sample_rate) / static_cast(stream_info.sample_rate); + + if (this->sample_ratio_ < 1.0) { + this->lowpass_ratio_ -= (10.24 / 16); + + if (this->lowpass_ratio_ < 0.84) { + this->lowpass_ratio_ = 0.84; + } + + if (this->lowpass_ratio_ < this->sample_ratio_) { + // avoid discontinuities near unity sample ratios + this->lowpass_ratio_ = this->sample_ratio_; + } + } + if (this->lowpass_ratio_ * this->sample_ratio_ < 0.98 && USE_PRE_POST_FILTER) { + float cutoff = this->lowpass_ratio_ * this->sample_ratio_ / 2.0; + biquad_lowpass(&this->lowpass_coeff_, cutoff); + this->pre_filter_ = true; + } + + if (this->lowpass_ratio_ / this->sample_ratio_ < 0.98 && USE_PRE_POST_FILTER && !this->pre_filter_) { + float cutoff = this->lowpass_ratio_ / this->sample_ratio_ / 2.0; + biquad_lowpass(&this->lowpass_coeff_, cutoff); + this->post_filter_ = true; + } + + if (this->pre_filter_ || this->post_filter_) { + for (int i = 0; i < stream_info.channels; ++i) { + biquad_init(&this->lowpass_[i][0], &this->lowpass_coeff_, 1.0); + biquad_init(&this->lowpass_[i][1], &this->lowpass_coeff_, 1.0); + } + } + + if (this->sample_ratio_ < 1.0) { + this->resampler_ = resampleInit(stream_info.channels, NUM_TAPS, NUM_FILTERS, + this->sample_ratio_ * this->lowpass_ratio_, flags | INCLUDE_LOWPASS); + } else if (this->lowpass_ratio_ < 1.0) { + this->resampler_ = + resampleInit(stream_info.channels, NUM_TAPS, NUM_FILTERS, this->lowpass_ratio_, flags | INCLUDE_LOWPASS); + } else { + this->resampler_ = resampleInit(stream_info.channels, NUM_TAPS, NUM_FILTERS, 1.0, flags); + } + + resampleAdvancePosition(this->resampler_, NUM_TAPS / 2.0); + + } else { + resample_info.resample = false; + } + + this->resample_info_ = resample_info; + return ESP_OK; +} + +AudioResamplerState AudioResampler::resample(bool stop_gracefully) { + if (stop_gracefully) { + if ((this->input_ring_buffer_->available() == 0) && (this->output_ring_buffer_->available() == 0) && + (this->input_buffer_length_ == 0) && (this->output_buffer_length_ == 0)) { + return AudioResamplerState::FINISHED; + } + } + + if (this->output_buffer_length_ > 0) { + size_t bytes_to_write = this->output_buffer_length_; + + if (bytes_to_write > 0) { + size_t bytes_written = this->output_ring_buffer_->write_without_replacement( + (void *) this->output_buffer_current_, bytes_to_write, pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS)); + + this->output_buffer_current_ += bytes_written / sizeof(int16_t); + this->output_buffer_length_ -= bytes_written; + } + + return AudioResamplerState::RESAMPLING; + } + + // Copy audio data directly to output_buffer if resampling isn't required + if (!this->resample_info_.resample && !this->resample_info_.mono_to_stereo) { + size_t bytes_read = + this->input_ring_buffer_->read((void *) this->output_buffer_, this->internal_buffer_samples_ * sizeof(int16_t), + pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS)); + + this->output_buffer_current_ = this->output_buffer_; + this->output_buffer_length_ += bytes_read; + + return AudioResamplerState::RESAMPLING; + } + + ////// + // Refill input buffer + ////// + + // Depending on if we are converting mono to stereo or if we are upsampling, we may need to restrict how many input + // samples we transfer + size_t max_input_samples = this->internal_buffer_samples_; + + // Mono to stereo -> cut in half + max_input_samples /= (2 / this->stream_info_.channels); + + if (this->sample_ratio_ > 1.0) { + // Upsampling -> reduce by a factor of the ceiling of sample_ratio_ + uint32_t upsampling_factor = std::ceil(this->sample_ratio_); + max_input_samples /= upsampling_factor; + } + + // Move old data to the start of the buffer + if (this->input_buffer_length_ > 0) { + memmove((void *) this->input_buffer_, (void *) this->input_buffer_current_, this->input_buffer_length_); + } + this->input_buffer_current_ = this->input_buffer_; + + // Copy new data to the end of the of the buffer + size_t bytes_to_read = max_input_samples * sizeof(int16_t) - this->input_buffer_length_; + + if (bytes_to_read > 0) { + int16_t *new_input_buffer_data = this->input_buffer_ + this->input_buffer_length_ / sizeof(int16_t); + size_t bytes_read = this->input_ring_buffer_->read((void *) new_input_buffer_data, bytes_to_read, + pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS)); + + this->input_buffer_length_ += bytes_read; + } + + if (this->input_buffer_length_ == 0) { + return AudioResamplerState::RESAMPLING; + } + + if (this->resample_info_.resample) { + if (this->input_buffer_length_ > 0) { + // Samples are indiviudal int16 values. Frames include 1 sample for mono and 2 samples for stereo + // Be careful converting between bytes, samples, and frames! + // 1 sample = 2 bytes = sizeof(int16_t) + // if mono: + // 1 frame = 1 sample + // if stereo: + // 1 frame = 2 samples (left and right) + + size_t samples_read = this->input_buffer_length_ / sizeof(int16_t); + + for (int i = 0; i < samples_read; ++i) { + this->float_input_buffer_[i] = static_cast(this->input_buffer_[i]) / 32768.0f; + } + + size_t frames_read = samples_read / this->stream_info_.channels; + + if (this->pre_filter_) { + for (int i = 0; i < this->stream_info_.channels; ++i) { + biquad_apply_buffer(&this->lowpass_[i][0], this->float_input_buffer_ + i, frames_read, + this->stream_info_.channels); + biquad_apply_buffer(&this->lowpass_[i][1], this->float_input_buffer_ + i, frames_read, + this->stream_info_.channels); + } + } + + ResampleResult res; + + res = resampleProcessInterleaved(this->resampler_, this->float_input_buffer_, frames_read, + this->float_output_buffer_, + this->internal_buffer_samples_ / this->channel_factor_, this->sample_ratio_); + + size_t frames_used = res.input_used; + size_t samples_used = frames_used * this->stream_info_.channels; + + size_t frames_generated = res.output_generated; + if (this->post_filter_) { + for (int i = 0; i < this->stream_info_.channels; ++i) { + biquad_apply_buffer(&this->lowpass_[i][0], this->float_output_buffer_ + i, frames_generated, + this->stream_info_.channels); + biquad_apply_buffer(&this->lowpass_[i][1], this->float_output_buffer_ + i, frames_generated, + this->stream_info_.channels); + } + } + + size_t samples_generated = frames_generated * this->stream_info_.channels; + + for (int i = 0; i < samples_generated; ++i) { + this->output_buffer_[i] = static_cast(this->float_output_buffer_[i] * 32767); + } + + this->input_buffer_current_ += samples_used; + this->input_buffer_length_ -= samples_used * sizeof(int16_t); + + this->output_buffer_current_ = this->output_buffer_; + this->output_buffer_length_ += samples_generated * sizeof(int16_t); + } + } else { + size_t bytes_to_transfer = + std::min(this->internal_buffer_samples_ / this->channel_factor_ * sizeof(int16_t), this->input_buffer_length_); + std::memcpy((void *) this->output_buffer_, (void *) this->input_buffer_current_, bytes_to_transfer); + + this->input_buffer_current_ += bytes_to_transfer / sizeof(int16_t); + this->input_buffer_length_ -= bytes_to_transfer; + + this->output_buffer_current_ = this->output_buffer_; + this->output_buffer_length_ += bytes_to_transfer; + } + + if (this->resample_info_.mono_to_stereo) { + // Convert mono to stereo + for (int i = this->output_buffer_length_ / (sizeof(int16_t)) - 1; i >= 0; --i) { + this->output_buffer_[2 * i] = this->output_buffer_[i]; + this->output_buffer_[2 * i + 1] = this->output_buffer_[i]; + } + + this->output_buffer_length_ *= 2; // double the bytes for stereo samples + } + return AudioResamplerState::RESAMPLING; +} + +} // namespace nabu +} // namespace esphome + +#endif \ No newline at end of file diff --git a/esphome/components/satellite1/media_player/audio_resampler.h b/esphome/components/satellite1/media_player/audio_resampler.h new file mode 100644 index 00000000..eb455d87 --- /dev/null +++ b/esphome/components/satellite1/media_player/audio_resampler.h @@ -0,0 +1,82 @@ +#pragma once + +#ifdef USE_ESP_IDF + +#include "biquad.h" +#include "resampler.h" + +#include "esphome/components/media_player/media_player.h" +#include "esphome/core/ring_buffer.h" + +namespace esphome { +namespace nabu { + +enum class AudioResamplerState : uint8_t { + INITIALIZED = 0, + RESAMPLING, + FINISHED, + FAILED, +}; + +struct ResampleInfo { + bool resample; + bool mono_to_stereo; +}; + +class AudioResampler { + public: + AudioResampler(esphome::RingBuffer *input_ring_buffer, esphome::RingBuffer *output_ring_buffer, + size_t internal_buffer_samples); + ~AudioResampler(); + + /// @brief Sets up the various bits necessary to resample + /// @param stream_info the incoming sample rate, bits per sample, and number of channels + /// @param target_sample_rate the necessary sample rate to convert to + /// @return ESP_OK if it is able to convert the incoming stream or an error otherwise + esp_err_t start(media_player::StreamInfo &stream_info, uint32_t target_sample_rate, ResampleInfo &resample_info); + + AudioResamplerState resample(bool stop_gracefully); + + protected: + esp_err_t allocate_buffers_(); + + esphome::RingBuffer *input_ring_buffer_; + esphome::RingBuffer *output_ring_buffer_; + size_t internal_buffer_samples_; + + int16_t *input_buffer_{nullptr}; + int16_t *input_buffer_current_{nullptr}; + size_t input_buffer_length_; + + int16_t *output_buffer_{nullptr}; + int16_t *output_buffer_current_{nullptr}; + size_t output_buffer_length_; + + float *float_input_buffer_{nullptr}; + float *float_input_buffer_current_{nullptr}; + size_t float_input_buffer_length_; + + float *float_output_buffer_{nullptr}; + float *float_output_buffer_current_{nullptr}; + size_t float_output_buffer_length_; + + media_player::StreamInfo stream_info_; + ResampleInfo resample_info_; + + Resample *resampler_{nullptr}; + + Biquad lowpass_[2][2]; + BiquadCoefficients lowpass_coeff_; + + float sample_ratio_{1.0}; + float lowpass_ratio_{1.0}; + uint8_t channel_factor_{1}; + + bool pre_filter_{false}; + bool post_filter_{false}; +}; + +} // namespace nabu +} // namespace esphome + +#endif \ No newline at end of file diff --git a/esphome/components/satellite1/media_player/biquad.c b/esphome/components/satellite1/media_player/biquad.c new file mode 100644 index 00000000..86dd43bd --- /dev/null +++ b/esphome/components/satellite1/media_player/biquad.c @@ -0,0 +1,95 @@ +//////////////////////////////////////////////////////////////////////////// +// **** BIQUAD **** // +// Simple Biquad Filter Library // +// Copyright (c) 2021 - 2022 David Bryant. // +// All Rights Reserved. // +// Distributed under the BSD Software License (see license.txt) // +//////////////////////////////////////////////////////////////////////////// + +// biquad.c + +#ifdef USE_ESP_IDF + +#include "biquad.h" + +// Second-order Lowpass + +void biquad_lowpass(BiquadCoefficients *filter, double frequency) { + double Q = sqrt(0.5), K = tan(M_PI * frequency); + double norm = 1.0 / (1.0 + K / Q + K * K); + + filter->a0 = K * K * norm; + filter->a1 = 2 * filter->a0; + filter->a2 = filter->a0; + filter->b1 = 2.0 * (K * K - 1.0) * norm; + filter->b2 = (1.0 - K / Q + K * K) * norm; +} + +// Second-order Highpass + +void biquad_highpass(BiquadCoefficients *filter, double frequency) { + double Q = sqrt(0.5), K = tan(M_PI * frequency); + double norm = 1.0 / (1.0 + K / Q + K * K); + + filter->a0 = norm; + filter->a1 = -2.0 * norm; + filter->a2 = filter->a0; + filter->b1 = 2.0 * (K * K - 1.0) * norm; + filter->b2 = (1.0 - K / Q + K * K) * norm; +} + +// Initialize the specified biquad filter with the given parameters. Note that the "gain" parameter is supplied here +// to save a multiply every time the filter in applied. + +void biquad_init(Biquad *f, const BiquadCoefficients *coeffs, float gain) { + f->coeffs = *coeffs; + f->coeffs.a0 *= gain; + f->coeffs.a1 *= gain; + f->coeffs.a2 *= gain; + f->in_d1 = f->in_d2 = 0.0F; + f->out_d1 = f->out_d2 = 0.0F; + f->first_order = (coeffs->a2 == 0.0F && coeffs->b2 == 0.0F); +} + +// Apply the supplied sample to the specified biquad filter, which must have been initialized with biquad_init(). + +float biquad_apply_sample(Biquad *f, float input) { + float sum; + + if (f->first_order) + sum = (input * f->coeffs.a0) + (f->in_d1 * f->coeffs.a1) - (f->coeffs.b1 * f->out_d1); + else + sum = (input * f->coeffs.a0) + (f->in_d1 * f->coeffs.a1) + (f->in_d2 * f->coeffs.a2) - (f->coeffs.b1 * f->out_d1) - + (f->coeffs.b2 * f->out_d2); + + f->out_d2 = f->out_d1; + f->out_d1 = sum; + f->in_d2 = f->in_d1; + f->in_d1 = input; + return sum; +} + +// Apply the supplied buffer to the specified biquad filter, which must have been initialized with biquad_init(). + +void biquad_apply_buffer(Biquad *f, float *buffer, int num_samples, int stride) { + if (f->first_order) + while (num_samples--) { + float sum = (*buffer * f->coeffs.a0) + (f->in_d1 * f->coeffs.a1) - (f->coeffs.b1 * f->out_d1); + f->out_d2 = f->out_d1; + f->in_d2 = f->in_d1; + f->in_d1 = *buffer; + *buffer = f->out_d1 = sum; + buffer += stride; + } + else + while (num_samples--) { + float sum = (*buffer * f->coeffs.a0) + (f->in_d1 * f->coeffs.a1) + (f->in_d2 * f->coeffs.a2) - + (f->coeffs.b1 * f->out_d1) - (f->coeffs.b2 * f->out_d2); + f->out_d2 = f->out_d1; + f->in_d2 = f->in_d1; + f->in_d1 = *buffer; + *buffer = f->out_d1 = sum; + buffer += stride; + } +} +#endif \ No newline at end of file diff --git a/esphome/components/satellite1/media_player/biquad.h b/esphome/components/satellite1/media_player/biquad.h new file mode 100644 index 00000000..e57aa527 --- /dev/null +++ b/esphome/components/satellite1/media_player/biquad.h @@ -0,0 +1,45 @@ +#pragma once +//////////////////////////////////////////////////////////////////////////// +// **** BIQUAD **** // +// Simple Biquad Filter Library // +// Copyright (c) 2021 - 2022 David Bryant. // +// All Rights Reserved. // +// Distributed under the BSD Software License (see license.txt) // +//////////////////////////////////////////////////////////////////////////// + +// biquad.h + +#ifdef USE_ESP_IDF + +#include +#include +#include +#include + +typedef struct { + float a0, a1, a2, b1, b2; +} BiquadCoefficients; + +typedef struct { + BiquadCoefficients coeffs; // coefficients + float in_d1, in_d2; // delayed input + float out_d1, out_d2; // delayed output + int first_order; // optimization +} Biquad; + +#ifdef __cplusplus +extern "C" { +#endif + +void biquad_init(Biquad *f, const BiquadCoefficients *coeffs, float gain); + +void biquad_lowpass(BiquadCoefficients *filter, double frequency); +void biquad_highpass(BiquadCoefficients *filter, double frequency); + +void biquad_apply_buffer(Biquad *f, float *buffer, int num_samples, int stride); +float biquad_apply_sample(Biquad *f, float input); + +#ifdef __cplusplus +} +#endif +#endif \ No newline at end of file diff --git a/esphome/components/satellite1/media_player/flac_decoder.cpp b/esphome/components/satellite1/media_player/flac_decoder.cpp new file mode 100644 index 00000000..6e10ee81 --- /dev/null +++ b/esphome/components/satellite1/media_player/flac_decoder.cpp @@ -0,0 +1,615 @@ +#ifdef USE_ESP_IDF + +#include +#include +#include +#include + +#include "flac_decoder.h" + +namespace flac { + +FLACDecoderResult FLACDecoder::read_header(size_t buffer_length) { + this->buffer_index_ = 0; + this->bytes_left_ = buffer_length; + this->bit_buffer_ = 0; + this->bit_buffer_length_ = 0; + + this->out_of_data_ = (buffer_length == 0); + + if (!this->partial_header_read_) { + // File must start with 'fLaC' + if (this->read_uint(32) != FLAC_MAGIC_NUMBER) { + return FLAC_DECODER_ERROR_BAD_MAGIC_NUMBER; + } + } + + while (!this->partial_header_last_ || (this->partial_header_length_ > 0)) { + if (this->bytes_left_ == 0) { + // We'll try to finish reading it once more data is loaded + this->partial_header_read_ = true; + return FLAC_DECODER_HEADER_OUT_OF_DATA; + } + + if (this->partial_header_length_ == 0) { + this->partial_header_last_ = this->read_uint(1) != 0; + this->partial_header_type_ = this->read_uint(7); + this->partial_header_length_ = this->read_uint(24); + } + + if (this->partial_header_type_ == 0) { + // Stream info block + this->min_block_size_ = this->read_uint(16); + this->max_block_size_ = this->read_uint(16); + this->read_uint(24); + this->read_uint(24); + + this->sample_rate_ = this->read_uint(20); + this->num_channels_ = this->read_uint(3) + 1; + this->sample_depth_ = this->read_uint(5) + 1; + this->num_samples_ = this->read_uint(36); + this->read_uint(128); + + this->partial_header_length_ = 0; + } else { + // Variable block + while (this->partial_header_length_ > 0) { + if (this->bytes_left_ == 0) { + break; + } + this->read_uint(8); + --this->partial_header_length_; + } + } // variable block + } + + if ((this->sample_rate_ == 0) || (this->num_channels_ == 0) || (this->sample_depth_ == 0) || + (this->max_block_size_ == 0)) { + return FLAC_DECODER_ERROR_BAD_HEADER; + } + + if ((this->min_block_size_ < 16 ) || (this->min_block_size_ > this->max_block_size_) || + (this->max_block_size_ > 65535)) { + return FLAC_DECODER_ERROR_BAD_HEADER; + } + + // Successfully read header + return FLAC_DECODER_SUCCESS; +} // read_header + + + +FLACDecoderResult FLACDecoder::frame_sync_(){ + this->frame_sync_bytes_[0] = 0; + this->frame_sync_bytes_[1] = 0; + + bool second_ff_byte_found = false; + uint32_t byte; + + this->align_to_byte(); + + while(true){ + if ( second_ff_byte_found ){ + //try if the prev found 0xff is first of the MAGIC NUMBER + byte = 0xff; + second_ff_byte_found = false; + } + else{ + byte = this->read_aligned_byte(); + } + if( byte == 0xff ){ + byte = this->read_aligned_byte(); + if( byte == 0xff ){ + //found a second 0xff, could be the first byte of the MAGIC NUMBER + second_ff_byte_found = true; + } + else if(byte >> 1 == 0x7c) { /* MAGIC NUMBER for the last 6 sync bits and reserved 7th bit */ + this->frame_sync_bytes_[0] = 0xff; + this->frame_sync_bytes_[1] = byte; + return FLAC_DECODER_SUCCESS; + } + } + else if (this->out_of_data_){ + return FLAC_DECODER_ERROR_SYNC_NOT_FOUND; + } + } + return FLAC_DECODER_ERROR_SYNC_NOT_FOUND; +} + + +FLACDecoderResult FLACDecoder::decode_frame_header_(){ + uint8_t raw_header[16]; + uint32_t raw_header_len = 0; + uint32_t new_byte; + + if( this->frame_sync_() != FLAC_DECODER_SUCCESS ){ + return FLAC_DECODER_ERROR_SYNC_NOT_FOUND; + } + + raw_header[raw_header_len++] = this->frame_sync_bytes_[0]; + raw_header[raw_header_len++] = this->frame_sync_bytes_[1]; + + /* make sure that reserved bit is 0 */ + if(raw_header[1] & 0x02){ /* MAGIC NUMBER */ + return FLAC_DECODER_ERROR_BAD_MAGIC_NUMBER; + } + + new_byte = this->read_aligned_byte(); + if(new_byte == 0xff) { /* MAGIC NUMBER for the first 8 frame sync bits */ + /* if we get here it means our original sync was erroneous since the sync code cannot appear in the header */ + // needs to search for sync code again + return FLAC_DECODER_ERROR_SYNC_NOT_FOUND; + } + raw_header[raw_header_len++] = new_byte; + + // 9.1.1 Block size bits + uint8_t block_size_code = raw_header[2] >> 4; + if (block_size_code == 0) { + return FLAC_DECODER_ERROR_BAD_BLOCK_SIZE_CODE; + } else if (block_size_code == 1) { + this->curr_frame_block_size_ = 192; + } else if ((2 <= block_size_code) && (block_size_code <= 5)) { + this->curr_frame_block_size_ = 576 << (block_size_code - 2); + } else if (block_size_code == 6) { + // uncommon block size + // gets parsed later + } else if (block_size_code == 7) { + // uncommon block size + // gets parsed later + } else if (block_size_code <= 15) { + this->curr_frame_block_size_ = 256 << (block_size_code - 8); + } else { + return FLAC_DECODER_ERROR_BAD_BLOCK_SIZE_CODE; + } + + // 9.1.2 Sample rate bits + // Assuming that we have sample rate from header + // indicates if uncommon sample rate needs to be parsed though + uint8_t sample_rate_code = raw_header[2] & 0x0f; + //assert( sample_rate_code == 0 || sample_rate_code == 0b1010 ); + + // 9.1.3 Channel bits + new_byte = this->read_aligned_byte(); + if(new_byte == 0xff) { /* MAGIC NUMBER for the first 8 frame sync bits */ + /* if we get here it means our original sync was erroneous since the sync code cannot appear in the header */ + // needs to search for sync code again + return FLAC_DECODER_ERROR_SYNC_NOT_FOUND; + } + raw_header[raw_header_len++] = new_byte; + this->curr_frame_channel_assign_ = raw_header[3] >> 4; + + // 9.1.4 Bit depth bits + uint8_t bits_per_sample_code = (raw_header[3] & 0x0e) >> 1; + switch( bits_per_sample_code){ + case 0: + //take bit depth from streaminfo header + break; + case 1: // 8 bit + case 2: // 12 bit + // not supported in this version + return FLAC_DECODER_ERROR_BAD_HEADER; + case 4: // 16 bit + break; + case 5: // 20bit + case 6: // 24bit + case 7: // 32bit + default: + // not supported in this version + return FLAC_DECODER_ERROR_BAD_HEADER; + } + + //reserved bit needs to be zero: + //ignore raw_header[3] & 0x01 != 0 + //seems not to be respected by all encoder versions + + + // 9.1.5. Coded number + //The coded number is stored in a variable length code like UTF-8 as defined in [RFC3629], but extended to a maximum of 36 bits unencoded, 7 bytes encoded. + // Interpretation depends on block_size_mode, signalled with (raw_header[1] & 0x01) + // We don't support file seeking for now so ignore the coded number + // todo: check for invalid codes, i.e. 0xffffffff (fixed block size) and 0xffffffffffffffff (variable block size) + uint32_t next_int = this->read_aligned_byte(); + raw_header[raw_header_len++] = next_int; + while (next_int >= 0b11000000 ) { + raw_header[raw_header_len++] = this->read_aligned_byte(); + next_int = (next_int << 1) & 0xFF; + } + + // 9.1.6 Uncommon block size + if (block_size_code == 6) { + raw_header[raw_header_len] = this->read_aligned_byte(); + this->curr_frame_block_size_ = raw_header[raw_header_len++] + 1; + } else if (block_size_code == 7) { + raw_header[raw_header_len] = this->read_aligned_byte(); + this->curr_frame_block_size_ = raw_header[raw_header_len++] << 8; + raw_header[raw_header_len] = this->read_aligned_byte(); + this->curr_frame_block_size_ |= raw_header[raw_header_len++]; + this->curr_frame_block_size_ += 1; + } + + // 9.1.7 Uncommon sample rate + // Assuming that we have sample rate from header + if (sample_rate_code == 12) { + raw_header[raw_header_len++] = this->read_aligned_byte(); + } else if ((sample_rate_code == 13) || (sample_rate_code == 14)) { + raw_header[raw_header_len++] = this->read_aligned_byte(); + raw_header[raw_header_len++] = this->read_aligned_byte(); + } + + // out of data wasn't checked after each read, check it now + if(this->out_of_data_){ + return FLAC_DECODER_ERROR_OUT_OF_DATA; + } + + // 9.1.8 Frame header CRC + uint8_t crc_read = this->read_aligned_byte(); + + return FLAC_DECODER_SUCCESS; +} + +FLACDecoderResult FLACDecoder::decode_frame(size_t buffer_length, int16_t *output_buffer, uint32_t *num_samples) { + this->buffer_index_ = 0; + this->bytes_left_ = buffer_length; + this->out_of_data_ = false; + + FLACDecoderResult ret = FLAC_DECODER_SUCCESS; + + *num_samples = 0; + + if (!this->block_samples_) { + // freed in free_buffers() + esphome::ExternalRAMAllocator allocator(esphome::ExternalRAMAllocator::ALLOW_FAILURE); + this->block_samples_ = allocator.allocate(this->max_block_size_ * this->num_channels_); + } + if (!this->block_samples_) { + return FLAC_DECODER_ERROR_MEMORY_ALLOCATION_ERROR; + } + + if (this->bytes_left_ == 0) { + // buffer is empty when called + return FLAC_DECODER_NO_MORE_FRAMES; + } + + uint64_t previous_bit_buffer = this->bit_buffer_; + uint32_t previous_bit_buffer_length = this->bit_buffer_length_; + ret = this->decode_frame_header_(); + if( ret != FLAC_DECODER_SUCCESS ){ + return ret; + } + + // Memory is allocated based on the maximum block size. + // Ensure that no out-of-bounds access occurs, particularly in case of parsing errors. + if( this->curr_frame_block_size_ > this->max_block_size_ ){ + return FLAC_DECODER_ERROR_BLOCK_SIZE_OUT_OF_RANGE; + } + + // Output buffer size (in sample) should be max_block_size * num_channels + this->decode_subframes(this->curr_frame_block_size_, this->sample_depth_, this->curr_frame_channel_assign_); + *num_samples = this->curr_frame_block_size_ * this->num_channels_; + + if (this->bytes_left_ < 2) { + this->bit_buffer_ = previous_bit_buffer; + this->bit_buffer_length_ = previous_bit_buffer_length; + return FLAC_DECODER_ERROR_OUT_OF_DATA; + } + + // Footer + this->align_to_byte(); + this->read_uint(16); + + int32_t addend = 0; + if (this->sample_depth_ == 8) { + addend = 128; + } + + // Copy samples to output buffer + std::size_t output_index = 0; + for (uint32_t i = 0; i < this->curr_frame_block_size_; i++) { + for (uint32_t j = 0; j < this->num_channels_; j++) { + output_buffer[output_index] = this->block_samples_[(j * this->curr_frame_block_size_) + i] + addend; + output_index++; + } + } + + return FLAC_DECODER_SUCCESS; +} // decode_frame + +void FLACDecoder::free_buffers() { + if (this->block_samples_) { + // delete this->block_samples_; + esphome::ExternalRAMAllocator allocator(esphome::ExternalRAMAllocator::ALLOW_FAILURE); + allocator.deallocate(this->block_samples_, this->max_block_size_ * this->num_channels_); + this->block_samples_ = nullptr; + } +} // free_buffers + +FLACDecoderResult FLACDecoder::decode_subframes(uint32_t block_size, uint32_t sample_depth, + uint32_t channel_assignment) { + FLACDecoderResult result = FLAC_DECODER_SUCCESS; + if (channel_assignment <= 7) { + std::size_t block_samples_offset = 0; + for (std::size_t i = 0; i < channel_assignment + 1; i++) { + result = this->decode_subframe(block_size, sample_depth, block_samples_offset); + if (result != FLAC_DECODER_SUCCESS) { + return result; + } + block_samples_offset += block_size; + } + } else if ((8 <= channel_assignment) && (channel_assignment <= 10)) { + result = this->decode_subframe(block_size, sample_depth + ((channel_assignment == 9) ? 1 : 0), 0); + if (result != FLAC_DECODER_SUCCESS) { + return result; + } + result = this->decode_subframe(block_size, sample_depth + ((channel_assignment == 9) ? 0 : 1), block_size); + if (result != FLAC_DECODER_SUCCESS) { + return result; + } + + if (channel_assignment == 8) { + for (std::size_t i = 0; i < block_size; i++) { + this->block_samples_[block_size + i] = this->block_samples_[i] - this->block_samples_[block_size + i]; + } + } else if (channel_assignment == 9) { + for (std::size_t i = 0; i < block_size; i++) { + this->block_samples_[i] += this->block_samples_[block_size + i]; + } + } else if (channel_assignment == 10) { + for (std::size_t i = 0; i < block_size; i++) { + int32_t side = this->block_samples_[block_size + i]; + int32_t right = this->block_samples_[i] - (side >> 1); + this->block_samples_[block_size + i] = right; + this->block_samples_[i] = right + side; + } + } + } else { + result = FLAC_DECODER_ERROR_RESERVED_CHANNEL_ASSIGNMENT; + } + + return result; +} // decode_subframes + +FLACDecoderResult FLACDecoder::decode_subframe(uint32_t block_size, uint32_t sample_depth, + std::size_t block_samples_offset) { + this->read_uint(1); + + uint32_t type = this->read_uint(6); + uint32_t shift = this->read_uint(1); + if (shift == 1) { + while (this->read_uint(1) == 0) { + shift += 1; + + if (this->out_of_data_) { + return FLAC_DECODER_ERROR_OUT_OF_DATA; + } + } + } + + sample_depth -= shift; + + FLACDecoderResult result = FLAC_DECODER_SUCCESS; + if (type == 0) { + // Constant + int32_t value = this->read_sint(sample_depth) << shift; + std::fill(this->block_samples_ + block_samples_offset, this->block_samples_ + block_samples_offset + block_size, value ); + } else if (type == 1) { + // Verbatim + for (std::size_t i = 0; i < block_size; i++) { + this->block_samples_[block_samples_offset + i] = (this->read_sint(sample_depth) << shift); + } + } else if ((8 <= type) && (type <= 12)) { + // Fixed prediction + result = this->decode_fixed_subframe(block_size, block_samples_offset, type - 8, sample_depth); + } else if ((32 <= type) && (type <= 63)) { + // LPC (linear predictive coding) + result = this->decode_lpc_subframe(block_size, block_samples_offset, type - 31, sample_depth); + if (result != FLAC_DECODER_SUCCESS) { + return result; + } + if (shift > 0) { + for (std::size_t i = 0; i < block_size; i++) { + this->block_samples_[block_samples_offset + i] <<= shift; + } + } + } else { + result = FLAC_DECODER_ERROR_RESERVED_SUBFRAME_TYPE; + } + + return result; +} // decode_subframe + +FLACDecoderResult FLACDecoder::decode_fixed_subframe(uint32_t block_size, std::size_t block_samples_offset, + uint32_t pre_order, uint32_t sample_depth) { + if (pre_order > 4) { + return FLAC_DECODER_ERROR_BAD_FIXED_PREDICTION_ORDER; + } + + FLACDecoderResult result = FLAC_DECODER_SUCCESS; + + int32_t* const sub_frame_buffer = this->block_samples_ + block_samples_offset; + int32_t *out_ptr = sub_frame_buffer; + + //warum-up samples + for (std::size_t i = 0; i < pre_order; i++) { + *(out_ptr++) = this->read_sint(sample_depth); + } + result = decode_residuals(sub_frame_buffer, pre_order, block_size); + if (result != FLAC_DECODER_SUCCESS) { + return result; + } + restore_linear_prediction(sub_frame_buffer, block_size, FLAC_FIXED_COEFFICIENTS[pre_order], 0); + return result; + +} // decode_fixed_subframe + +FLACDecoderResult FLACDecoder::decode_lpc_subframe(uint32_t block_size, std::size_t block_samples_offset, + uint32_t lpc_order, uint32_t sample_depth) { + FLACDecoderResult result = FLAC_DECODER_SUCCESS; + + int32_t* const sub_frame_buffer = this->block_samples_ + block_samples_offset; + int32_t* out_ptr = sub_frame_buffer; + + for (std::size_t i = 0; i < lpc_order; i++) { + *(out_ptr++) = this->read_sint(sample_depth); + } + + uint32_t precision = this->read_uint(4) + 1; + int32_t shift = this->read_sint(5); + + std::vector coefs; + coefs.resize(lpc_order + 1); + for (std::size_t i = 0; i < lpc_order; i++) { + coefs[lpc_order - i - 1] = this->read_sint(precision); + } + coefs[lpc_order] = 1 << shift; + + result = decode_residuals(sub_frame_buffer, lpc_order, block_size); + if (result != FLAC_DECODER_SUCCESS) { + return result; + } + restore_linear_prediction(sub_frame_buffer, block_size, coefs, shift); + + return result; +} // decode_lpc_subframe + +FLACDecoderResult FLACDecoder::decode_residuals(int32_t* sub_frame_buffer, size_t warm_up_samples, uint32_t block_size) { + uint32_t method = this->read_uint(2); + if (method >= 2) { + return FLAC_DECODER_ERROR_RESERVED_RESIDUAL_CODING_METHOD; + } + + uint32_t param_bits = 4; + uint32_t escape_param = 0xF; + if (method == 1) { + param_bits = 5; + escape_param = 0x1F; + } + + uint32_t partition_order = this->read_uint(4); + uint32_t num_partitions = 1 << partition_order; + if ((block_size % num_partitions) != 0) { + return FLAC_DECODER_ERROR_BLOCK_SIZE_NOT_DIVISIBLE_RICE; + } + + int32_t *out_ptr = sub_frame_buffer + warm_up_samples; + { + uint32_t count = (block_size >> partition_order) - warm_up_samples; + uint32_t param = this->read_uint(param_bits); + if (param < escape_param) { + for (std::size_t j = 0; j < count; j++) { + *(out_ptr++) = this->read_rice_sint(param); + } + } else { + std::size_t num_bits = this->read_uint(5); + if( num_bits == 0 ){ + std::memset( out_ptr, 0, count * sizeof(int32_t)); + out_ptr += count; + } else { + for (std::size_t j = 0; j < count; j++) { + *(out_ptr++) = this->read_sint(num_bits); + } + } + } + } + + uint32_t count = block_size >> partition_order; + for (std::size_t i = 1; i < num_partitions; i++) { + uint32_t param = this->read_uint(param_bits); + if (param < escape_param) { + for (std::size_t j = 0; j < count; j++) { + *(out_ptr++) = this->read_rice_sint(param); + } + } else { + std::size_t num_bits = this->read_uint(5); + if( num_bits == 0 ){ + std::memset( out_ptr, 0, count * sizeof(int32_t)); + out_ptr += count; + } else { + for (std::size_t j = 0; j < count; j++) { + *(out_ptr++) = this->read_sint(num_bits); + } + } + } + } // for each partition + + return FLAC_DECODER_SUCCESS; +} // decode_residuals + +void FLACDecoder::restore_linear_prediction(int32_t* sub_frame_buffer, size_t num_of_samples, const std::vector &coefs, int32_t shift) { + + for (std::size_t i = 0; i < num_of_samples - coefs.size() + 1; i++) { + int32_t sum = 0; + for (std::size_t j = 0; j < coefs.size(); ++j) { + sum += (sub_frame_buffer[i + j] * coefs[j]); + } + sub_frame_buffer[i + coefs.size() - 1] = (sum >> shift); + } +} // restore_linear_prediction + +uint32_t FLACDecoder::read_aligned_byte(){ + //assumes byte alignment + assert( this->bit_buffer_length_ % 8 == 0 ); + + if( this->bit_buffer_length_ >= 8 ){ + this->bit_buffer_length_ -=8; + uint32_t ret_byte = this->bit_buffer_ >> this->bit_buffer_length_; + return ret_byte & FLAC_UINT_MASK[8]; + } + + if (this->bytes_left_ == 0) { + this->out_of_data_ = true; + return 0; + } + + uint8_t next_byte = this->buffer_[this->buffer_index_]; + this->buffer_index_++; + this->bytes_left_--; + + return next_byte; + +} + +uint32_t FLACDecoder::read_uint(std::size_t num_bits) { + while (this->bit_buffer_length_ < num_bits) { + if (this->bytes_left_ == 0) { + this->out_of_data_ = true; + return 0; + } + uint8_t next_byte = this->buffer_[this->buffer_index_]; + this->buffer_index_++; + this->bytes_left_--; + + this->bit_buffer_ = (this->bit_buffer_ << 8) | next_byte; + this->bit_buffer_length_ += 8; + } + + this->bit_buffer_length_ -= num_bits; + uint32_t result = this->bit_buffer_ >> this->bit_buffer_length_; + if (num_bits < 32) { + result &= FLAC_UINT_MASK[num_bits]; + } + + return result; +} // read_uint + +int32_t FLACDecoder::read_sint(std::size_t num_bits) { + uint32_t next_int = this->read_uint(num_bits); + return (int32_t) next_int - (((int32_t) next_int >> (num_bits - 1)) << num_bits); +} // read_sint + +// why int64_t? standard restricts residuals to fit into 32bit +int64_t FLACDecoder::read_rice_sint(uint8_t param) { + long value = 0; + while (this->read_uint(1) == 0) { + value++; + if (this->out_of_data_) { + return 0; + } + } + value = (value << param) | this->read_uint(param); + return (value >> 1) ^ -(value & 1); +} // read_rice_sint + +void FLACDecoder::align_to_byte() { this->bit_buffer_length_ -= (this->bit_buffer_length_ % 8); } // align_to_byte + +} // namespace flac +#endif \ No newline at end of file diff --git a/esphome/components/satellite1/media_player/flac_decoder.h b/esphome/components/satellite1/media_player/flac_decoder.h new file mode 100644 index 00000000..3ca38b0d --- /dev/null +++ b/esphome/components/satellite1/media_player/flac_decoder.h @@ -0,0 +1,200 @@ +// Port of: +// https://www.nayuki.io/res/simple-flac-implementation/simple-decode-flac-to-wav.py +// +// Uses some small parts from: https://github.com/schreibfaul1/ESP32-audioI2S/ +// See also: https://xiph.org/flac/format.html + +#ifdef USE_ESP_IDF + +#ifndef _FLAC_DECODER_H +#define _FLAC_DECODER_H + +#include "esphome/core/helpers.h" +#include "esphome/core/ring_buffer.h" + +#include + +namespace flac { + +// 'fLaC' +const static uint32_t FLAC_MAGIC_NUMBER = 0x664C6143; + +const static uint32_t FLAC_UINT_MASK[] = { + 0x00000000, 0x00000001, 0x00000003, 0x00000007, 0x0000000f, 0x0000001f, 0x0000003f, 0x0000007f, 0x000000ff, + 0x000001ff, 0x000003ff, 0x000007ff, 0x00000fff, 0x00001fff, 0x00003fff, 0x00007fff, 0x0000ffff, 0x0001ffff, + 0x0003ffff, 0x0007ffff, 0x000fffff, 0x001fffff, 0x003fffff, 0x007fffff, 0x00ffffff, 0x01ffffff, 0x03ffffff, + 0x07ffffff, 0x0fffffff, 0x1fffffff, 0x3fffffff, 0x7fffffff, 0xffffffff}; + +enum FLACDecoderResult { + FLAC_DECODER_SUCCESS = 0, + FLAC_DECODER_NO_MORE_FRAMES = 1, + FLAC_DECODER_HEADER_OUT_OF_DATA = 2, + FLAC_DECODER_ERROR_OUT_OF_DATA = 3, + FLAC_DECODER_ERROR_BAD_MAGIC_NUMBER = 4, + FLAC_DECODER_ERROR_SYNC_NOT_FOUND = 5, + FLAC_DECODER_ERROR_BAD_BLOCK_SIZE_CODE = 6, + FLAC_DECODER_ERROR_BAD_HEADER = 7, + FLAC_DECODER_ERROR_RESERVED_CHANNEL_ASSIGNMENT = 8, + FLAC_DECODER_ERROR_RESERVED_SUBFRAME_TYPE = 9, + FLAC_DECODER_ERROR_BAD_FIXED_PREDICTION_ORDER = 10, + FLAC_DECODER_ERROR_RESERVED_RESIDUAL_CODING_METHOD = 11, + FLAC_DECODER_ERROR_BLOCK_SIZE_NOT_DIVISIBLE_RICE = 12, + FLAC_DECODER_ERROR_MEMORY_ALLOCATION_ERROR = 13, + FLAC_DECODER_ERROR_BLOCK_SIZE_OUT_OF_RANGE = 14 +}; + +// Coefficients for fixed linear prediction +const static std::vector FLAC_FIXED_COEFFICIENTS[] = { + {1}, {1, 1}, {-1, 2, 1}, {1, -3, 3, 1}, {-1, 4, -6, 4, 1}}; + +/* Basic FLAC decoder ported from: + * https://www.nayuki.io/res/simple-flac-implementation/simple-decode-flac-to-wav.py + */ +class FLACDecoder { + public: + /* buffer - FLAC data + * buffer_size - size of the data buffer + * min_buffer_size - min bytes in buffer before fill_buffer is called + */ + FLACDecoder(uint8_t *buffer) : buffer_(buffer) {} + + ~FLACDecoder() { this->free_buffers(); } + + /* Reads FLAC header from buffer. + * Must be called before decode_frame. */ + FLACDecoderResult read_header(size_t buffer_length); + + /* Decodes a single frame of audio. + * Copies num_samples into output_buffer. + * Use get_output_buffer_size() to allocate output_buffer. */ + FLACDecoderResult decode_frame(size_t buffer_length, int16_t *output_buffer, uint32_t *num_samples); + + /* Frees internal memory. */ + void free_buffers(); + + /* Sample rate (after read_header()) */ + uint32_t get_sample_rate() { return this->sample_rate_; } + + /* Sample depth (after read_header()) */ + uint32_t get_sample_depth() { return this->sample_depth_; } + + /* Number of audio channels (after read_header()) */ + uint32_t get_num_channels() { return this->num_channels_; } + + /* Number of audio samples (after read_header()) */ + uint32_t get_num_samples() { return this->num_samples_; } + + /* Number of audio samples (after read_header()) */ + uint32_t get_min_block_size() { return this->min_block_size_; } + /* Number of audio samples (after read_header()) */ + uint32_t get_max_block_size() { return this->max_block_size_; } + + + /* Maximum number of output samples per frame (after read_header()) */ + uint32_t get_output_buffer_size() { return this->max_block_size_ * this->num_channels_; } + + /* Maximum number of output samples per frame (after read_header()) */ + uint32_t get_output_buffer_size_bytes() { return this->max_block_size_ * this->num_channels_ * this->sample_depth_ / 8; } + + std::size_t get_bytes_index() { return this->buffer_index_; } + + /* Number of unread bytes in the input buffer. */ + std::size_t get_bytes_left() { return this->bytes_left_; } + + protected: + FLACDecoderResult frame_sync_(); + + FLACDecoderResult decode_frame_header_(); + + /* Decodes one or more subframes by type. */ + FLACDecoderResult decode_subframes(uint32_t block_size, uint32_t sample_depth, uint32_t channel_assignment); + + /* Decodes a subframe by type. */ + FLACDecoderResult decode_subframe(uint32_t block_size, uint32_t sample_depth, std::size_t block_samples_offset); + + /* Decodes a subframe with fixed coefficients. */ + FLACDecoderResult decode_fixed_subframe(uint32_t block_size, std::size_t block_samples_offset, uint32_t pre_order, + uint32_t sample_depth); + + /* Decodes a subframe with dynamic coefficients. */ + FLACDecoderResult decode_lpc_subframe(uint32_t block_size, std::size_t block_samples_offset, uint32_t lpc_order, + uint32_t sample_depth); + + /* Decodes prediction residuals. */ + FLACDecoderResult decode_residuals(int32_t* buffer, size_t warm_up_samples, uint32_t block_size); + + /* Completes predicted samples. */ + void restore_linear_prediction(int32_t* sub_frame_buffer, size_t num_of_samples, const std::vector &coefs, int32_t shift); + + bool wait_for_bytes_(uint32_t num_of_bytes, TickType_t ticks_to_wait ); + + uint32_t read_aligned_byte(); + + /* Reads an unsigned integer of arbitrary bit size. */ + uint32_t read_uint(std::size_t num_bits); + + /* Reads a singed integer of arbitrary bit size. */ + int32_t read_sint(std::size_t num_bits); + + /* Reads a rice-encoded signed integer. */ + int64_t read_rice_sint(uint8_t param); + + /* Forces input buffer to be byte-aligned. */ + void align_to_byte(); + + private: + /* Pointer to input buffer with FLAC data. */ + uint8_t *buffer_ = nullptr; + + /* Next index to read from the input buffer. */ + std::size_t buffer_index_ = 0; + + /* Number of byte that haven't been read from the input buffer yet. */ + std::size_t bytes_left_ = 0; + + /* Number of bits in the bit buffer. */ + std::size_t bit_buffer_length_ = 0; + + /* Last read bits from input buffer. */ + uint64_t bit_buffer_ = 0; + + /* True if input buffer is empty and cannot be filled. */ + bool out_of_data_ = false; + + /* Minimum number of samples in a block (single channel). */ + uint32_t min_block_size_ = 0; + + /* Maximum number of samples in a block (single channel). */ + uint32_t max_block_size_ = 0; + + uint32_t curr_frame_block_size_ = 0; + uint32_t curr_frame_channel_assign_ = 0; + + /* Sample rate in hertz. */ + uint32_t sample_rate_ = 0; + + /* Number of audio channels. */ + uint32_t num_channels_ = 0; + + /* Bits per sample. */ + uint32_t sample_depth_ = 0; + + /* Total number of samples in the stream. */ + uint32_t num_samples_ = 0; + + /* Buffer of decoded samples at full precision (all channels). */ + int32_t *block_samples_ = nullptr; + + bool partial_header_read_{false}; + bool partial_header_last_{false}; + uint32_t partial_header_type_{0}; + uint32_t partial_header_length_{0}; + + bool frame_sync_found_{false}; + uint8_t frame_sync_bytes_[2]; +}; + +} // namespace flac + +#endif +#endif \ No newline at end of file diff --git a/esphome/components/satellite1/media_player/mp3_decoder.cpp b/esphome/components/satellite1/media_player/mp3_decoder.cpp new file mode 100644 index 00000000..720c16ab --- /dev/null +++ b/esphome/components/satellite1/media_player/mp3_decoder.cpp @@ -0,0 +1,8870 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: RCSL 1.0/RPSL 1.0 + * + * Portions Copyright (c) 1995-2002 RealNetworks, Inc. All Rights Reserved. + * + * The contents of this file, and the files included with this file, are + * subject to the current version of the RealNetworks Public Source License + * Version 1.0 (the "RPSL") available at + * http://www.helixcommunity.org/content/rpsl unless you have licensed + * the file under the RealNetworks Community Source License Version 1.0 + * (the "RCSL") available at http://www.helixcommunity.org/content/rcsl, + * in which case the RCSL will apply. You may also obtain the license terms + * directly from RealNetworks. You may not use this file except in + * compliance with the RPSL or, if you have a valid RCSL with RealNetworks + * applicable to this file, the RCSL. Please see the applicable RPSL or + * RCSL for the rights, obligations and limitations governing use of the + * contents of the file. + * + * This file is part of the Helix DNA Technology. RealNetworks is the + * developer of the Original Code and owns the copyrights in the portions + * it created. + * + * This file, and the files included with this file, is distributed and made + * available on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER + * EXPRESS OR IMPLIED, AND REALNETWORKS HEREBY DISCLAIMS ALL SUCH WARRANTIES, + * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. + * + * Technology Compatibility Kit Test Suite(s) Location: + * http://www.helixcommunity.org/content/tck + * + * Contributor(s): + * + * ***** END LICENSE BLOCK ***** */ + +/************************************************************************************** + * Fixed-point MP3 decoder + * Jon Recker (jrecker@real.com), Ken Cooke (kenc@real.com) + * June 2003 + * + **************************************************************************************/ + +#ifdef USE_ESP_IDF + +#include "mp3_decoder.h" +#include "esphome/core/helpers.h" + +/* indexing = [version][samplerate index] + * sample rate of frame (Hz) + */ +const int samplerateTab[3][3] = { + {44100, 48000, 32000}, /* MPEG-1 */ + {22050, 24000, 16000}, /* MPEG-2 */ + {11025, 12000, 8000}, /* MPEG-2.5 */ +}; + +/* indexing = [version][layer][bitrate index] + * bitrate (kbps) of frame + * - bitrate index == 0 is "free" mode (bitrate determined on the fly by + * counting bits between successive sync words) + */ +const short bitrateTab[3][3][15] = { + { + /* MPEG-1 */ + {0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448}, /* Layer 1 */ + {0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384}, /* Layer 2 */ + {0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320}, /* Layer 3 */ + }, + { + /* MPEG-2 */ + {0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256}, /* Layer 1 */ + {0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160}, /* Layer 2 */ + {0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160}, /* Layer 3 */ + }, + { + /* MPEG-2.5 */ + {0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256}, /* Layer 1 */ + {0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160}, /* Layer 2 */ + {0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160}, /* Layer 3 */ + }, +}; + +/* indexing = [version][layer] + * number of samples in one frame (per channel) + */ +const short samplesPerFrameTab[3][3] = { + {384, 1152, 1152}, /* MPEG1 */ + {384, 1152, 576}, /* MPEG2 */ + {384, 1152, 576}, /* MPEG2.5 */ +}; + +/* layers 1, 2, 3 */ +const short bitsPerSlotTab[3] = {32, 8, 8}; + +/* indexing = [version][mono/stereo] + * number of bytes in side info section of bitstream + */ +const short sideBytesTab[3][2] = { + {17, 32}, /* MPEG-1: mono, stereo */ + {9, 17}, /* MPEG-2: mono, stereo */ + {9, 17}, /* MPEG-2.5: mono, stereo */ +}; + +/* indexing = [version][sampleRate][bitRate] + * for layer3, nSlots = floor(samps/frame * bitRate / sampleRate / 8) + * - add one pad slot if necessary + */ +const short slotTab[3][3][15] = { + { + /* MPEG-1 */ + {0, 104, 130, 156, 182, 208, 261, 313, 365, 417, 522, 626, 731, 835, 1044}, /* 44 kHz */ + {0, 96, 120, 144, 168, 192, 240, 288, 336, 384, 480, 576, 672, 768, 960}, /* 48 kHz */ + {0, 144, 180, 216, 252, 288, 360, 432, 504, 576, 720, 864, 1008, 1152, 1440}, /* 32 kHz */ + }, + { + /* MPEG-2 */ + {0, 26, 52, 78, 104, 130, 156, 182, 208, 261, 313, 365, 417, 470, 522}, /* 22 kHz */ + {0, 24, 48, 72, 96, 120, 144, 168, 192, 240, 288, 336, 384, 432, 480}, /* 24 kHz */ + {0, 36, 72, 108, 144, 180, 216, 252, 288, 360, 432, 504, 576, 648, 720}, /* 16 kHz */ + }, + { + /* MPEG-2.5 */ + {0, 52, 104, 156, 208, 261, 313, 365, 417, 522, 626, 731, 835, 940, 1044}, /* 11 kHz */ + {0, 48, 96, 144, 192, 240, 288, 336, 384, 480, 576, 672, 768, 864, 960}, /* 12 kHz */ + {0, 72, 144, 216, 288, 360, 432, 504, 576, 720, 864, 1008, 1152, 1296, 1440}, /* 8 kHz */ + }, +}; + +/* indexing = [version][sampleRate][long (.l) or short (.s) block] + * sfBandTable[v][s].l[cb] = index of first bin in critical band cb (long + * blocks) sfBandTable[v][s].s[cb] = index of first bin in critical band cb + * (short blocks) + */ +const SFBandTable sfBandTable[3][3] = { + {/* MPEG-1 (44, 48, 32 kHz) */ + {{0, 4, 8, 12, 16, 20, 24, 30, 36, 44, 52, 62, 74, 90, 110, 134, 162, 196, 238, 288, 342, 418, 576}, + {0, 4, 8, 12, 16, 22, 30, 40, 52, 66, 84, 106, 136, 192}}, + {{0, 4, 8, 12, 16, 20, 24, 30, 36, 42, 50, 60, 72, 88, 106, 128, 156, 190, 230, 276, 330, 384, 576}, + {0, 4, 8, 12, 16, 22, 28, 38, 50, 64, 80, 100, 126, 192}}, + {{0, 4, 8, 12, 16, 20, 24, 30, 36, 44, 54, 66, 82, 102, 126, 156, 194, 240, 296, 364, 448, 550, 576}, + {0, 4, 8, 12, 16, 22, 30, 42, 58, 78, 104, 138, 180, 192}}}, + + { + /* MPEG-2 (22, 24, 16 kHz) */ + {{0, 6, 12, 18, 24, 30, 36, 44, 54, 66, 80, 96, 116, 140, 168, 200, 238, 284, 336, 396, 464, 522, 576}, + {0, 4, 8, 12, 18, 24, 32, 42, 56, 74, 100, 132, 174, 192}}, + {{0, 6, 12, 18, 24, 30, 36, 44, 54, 66, 80, 96, 114, 136, 162, 194, 232, 278, 332, 394, 464, 540, 576}, + {0, 4, 8, 12, 18, 26, 36, 48, 62, 80, 104, 136, 180, 192}}, + {{0, 6, 12, 18, 24, 30, 36, 44, 54, 66, 80, 96, 116, 140, 168, 200, 238, 284, 336, 396, 464, 522, 576}, + {0, 4, 8, 12, 18, 26, 36, 48, 62, 80, 104, 134, 174, 192}}, + }, + + { + /* MPEG-2.5 (11, 12, 8 kHz) */ + {{0, 6, 12, 18, 24, 30, 36, 44, 54, 66, 80, 96, 116, 140, 168, 200, 238, 284, 336, 396, 464, 522, 576}, + {0, 4, 8, 12, 18, 26, 36, 48, 62, 80, 104, 134, 174, 192}}, + {{0, 6, 12, 18, 24, 30, 36, 44, 54, 66, 80, 96, 116, 140, 168, 200, 238, 284, 336, 396, 464, 522, 576}, + {0, 4, 8, 12, 18, 26, 36, 48, 62, 80, 104, 134, 174, 192}}, + {{0, 12, 24, 36, 48, 60, 72, 88, 108, 132, 160, 192, 232, 280, 336, 400, 476, 566, 568, 570, 572, 574, 576}, + {0, 8, 16, 24, 36, 52, 72, 96, 124, 160, 162, 164, 166, 192}}, + }, +}; + +const uint32_t imdctWin[4][36] = { + { + 0x02aace8b, 0x07311c28, 0x0a868fec, 0x0c913b52, 0x0d413ccd, 0x0c913b52, 0x0a868fec, 0x07311c28, 0x02aace8b, + 0xfd16d8dd, 0xf6a09e66, 0xef7a6275, 0xe7dbc161, 0xe0000000, 0xd8243e9f, 0xd0859d8b, 0xc95f619a, 0xc2e92723, + 0xbd553175, 0xb8cee3d8, 0xb5797014, 0xb36ec4ae, 0xb2bec333, 0xb36ec4ae, 0xb5797014, 0xb8cee3d8, 0xbd553175, + 0xc2e92723, 0xc95f619a, 0xd0859d8b, 0xd8243e9f, 0xe0000000, 0xe7dbc161, 0xef7a6275, 0xf6a09e66, 0xfd16d8dd, + }, + { + 0x02aace8b, 0x07311c28, 0x0a868fec, 0x0c913b52, 0x0d413ccd, 0x0c913b52, 0x0a868fec, 0x07311c28, 0x02aace8b, + 0xfd16d8dd, 0xf6a09e66, 0xef7a6275, 0xe7dbc161, 0xe0000000, 0xd8243e9f, 0xd0859d8b, 0xc95f619a, 0xc2e92723, + 0xbd44ef14, 0xb831a052, 0xb3aa3837, 0xafb789a4, 0xac6145bb, 0xa9adecdc, 0xa864491f, 0xad1868f0, 0xb8431f49, + 0xc8f42236, 0xdda8e6b1, 0xf47755dc, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, + }, + { + 0x07311c28, 0x0d413ccd, 0x07311c28, 0xf6a09e66, 0xe0000000, 0xc95f619a, 0xb8cee3d8, 0xb2bec333, 0xb8cee3d8, + 0xc95f619a, 0xe0000000, 0xf6a09e66, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, + 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, + 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, + }, + { + 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x028e9709, 0x04855ec0, 0x026743a1, + 0xfcde2c10, 0xf515dc82, 0xec93e53b, 0xe4c880f8, 0xdd5d0b08, 0xd63510b7, 0xcf5e834a, 0xc8e6b562, 0xc2da4105, + 0xbd553175, 0xb8cee3d8, 0xb5797014, 0xb36ec4ae, 0xb2bec333, 0xb36ec4ae, 0xb5797014, 0xb8cee3d8, 0xbd553175, + 0xc2e92723, 0xc95f619a, 0xd0859d8b, 0xd8243e9f, 0xe0000000, 0xe7dbc161, 0xef7a6275, 0xf6a09e66, 0xfd16d8dd, + }, +}; + +/* indexing = [mid-side off/on][intensity scale factor] + * format = Q30, range = [0.0, 1.414] + * + * mid-side off: + * ISFMpeg1[0][i] = tan(i*pi/12) / [1 + tan(i*pi/12)] (left scalefactor) + * = 1 / [1 + tan(i*pi/12)] (right scalefactor) + * + * mid-side on: + * ISFMpeg1[1][i] = sqrt(2) * ISFMpeg1[0][i] + * + * output L = ISFMpeg1[midSide][isf][0] * input L + * output R = ISFMpeg1[midSide][isf][1] * input L + * + * obviously left scalefactor + right scalefactor = 1 (m-s off) or sqrt(2) (m-s + * on) so just store left and calculate right as 1 - left (can derive as right = + * ISFMpeg1[x][6] - left) + * + * if mid-side enabled, multiply joint stereo scale factors by sqrt(2) + * - we scaled whole spectrum by 1/sqrt(2) in Dequant for the M+S/sqrt(2) in + * MidSideProc + * - but the joint stereo part of the spectrum doesn't need this, so we have + * to undo it + * + * if scale factor is and illegal intensity position, this becomes a passthrough + * - gain = [1, 0] if mid-side off, since L is coded directly and R = 0 in + * this region + * - gain = [1, 1] if mid-side on, since L = (M+S)/sqrt(2), R = (M-S)/sqrt(2) + * - and since S = 0 in the joint stereo region (above NZB right) then L = R + * = M * 1.0 + */ +const int ISFMpeg1[2][7] = {{0x00000000, 0x0d8658ba, 0x176cf5d0, 0x20000000, 0x28930a2f, 0x3279a745, 0x40000000}, + {0x00000000, 0x13207f5c, 0x2120fb83, 0x2d413ccc, 0x39617e16, 0x4761fa3d, 0x5a827999}}; + +/* indexing = [intensity scale on/off][mid-side off/on][intensity scale factor] + * format = Q30, range = [0.0, 1.414] + * + * if (isf == 0) kl = 1.0 kr = 1.0 + * else if (isf & 0x01 == 0x01) kl = i0^((isf+1)/2), kr = 1.0 + * else if (isf & 0x01 == 0x00) kl = 1.0, kr = i0^(isf/2) + * + * if (intensityScale == 1) i0 = 1/sqrt(2) = 0x2d413ccc (Q30) + * else i0 = 1/sqrt(sqrt(2)) = 0x35d13f32 (Q30) + * + * see comments for ISFMpeg1 (just above) regarding scaling, sqrt(2), etc. + * + * compress the MPEG2 table using the obvious identities above... + * for isf = [0, 1, 2, ... 30], let sf = table[(isf+1) >> 1] + * - if isf odd, L = sf*L, R = tab[0]*R + * - if isf even, L = tab[0]*L, R = sf*R + */ +const int ISFMpeg2[2][2][16] = {{ + { + /* intensityScale off, mid-side off */ + 0x40000000, + 0x35d13f32, + 0x2d413ccc, + 0x260dfc14, + 0x1fffffff, + 0x1ae89f99, + 0x16a09e66, + 0x1306fe0a, + 0x0fffffff, + 0x0d744fcc, + 0x0b504f33, + 0x09837f05, + 0x07ffffff, + 0x06ba27e6, + 0x05a82799, + 0x04c1bf82, + }, + { + /* intensityScale off, mid-side on */ + 0x5a827999, + 0x4c1bf827, + 0x3fffffff, + 0x35d13f32, + 0x2d413ccc, + 0x260dfc13, + 0x1fffffff, + 0x1ae89f99, + 0x16a09e66, + 0x1306fe09, + 0x0fffffff, + 0x0d744fcc, + 0x0b504f33, + 0x09837f04, + 0x07ffffff, + 0x06ba27e6, + }, + }, + {{ + /* intensityScale on, mid-side off */ + 0x40000000, + 0x2d413ccc, + 0x20000000, + 0x16a09e66, + 0x10000000, + 0x0b504f33, + 0x08000000, + 0x05a82799, + 0x04000000, + 0x02d413cc, + 0x02000000, + 0x016a09e6, + 0x01000000, + 0x00b504f3, + 0x00800000, + 0x005a8279, + }, + /* intensityScale on, mid-side on */ + { + 0x5a827999, + 0x3fffffff, + 0x2d413ccc, + 0x1fffffff, + 0x16a09e66, + 0x0fffffff, + 0x0b504f33, + 0x07ffffff, + 0x05a82799, + 0x03ffffff, + 0x02d413cc, + 0x01ffffff, + 0x016a09e6, + 0x00ffffff, + 0x00b504f3, + 0x007fffff, + }}}; + +/* indexing = [intensity scale on/off][left/right] + * format = Q30, range = [0.0, 1.414] + * + * illegal intensity position scalefactors (see comments on ISFMpeg1) + */ +const int ISFIIP[2][2] = { + {0x40000000, 0x00000000}, /* mid-side off */ + {0x40000000, 0x40000000}, /* mid-side on */ +}; + +const unsigned char uniqueIDTab[8] = {0x5f, 0x4b, 0x43, 0x5f, 0x5f, 0x4a, 0x52, 0x5f}; + +/* anti-alias coefficients - see spec Annex B, table 3-B.9 + * csa[0][i] = CSi, csa[1][i] = CAi + * format = Q31 + */ +const uint32_t csa[8][2] = { + {0x6dc253f0, 0xbe2500aa}, {0x70dcebe4, 0xc39e4949}, {0x798d6e73, 0xd7e33f4a}, {0x7ddd40a7, 0xe8b71176}, + {0x7f6d20b7, 0xf3e4fe2f}, {0x7fe47e40, 0xfac1a3c7}, {0x7ffcb263, 0xfe2ebdc6}, {0x7fffc694, 0xff86c25d}, +}; + +/* format = Q30, range = [0.0981, 1.9976] + * + * n = 16; + * k = 0; + * for(i=0; i<5; i++, n=n/2) { + * for(p=0; p> 2, 31); /* smallest input scale = -47, so smallest scalei = -12 */ + + /* cache first 4 values */ + shift = MIN(scalei + 3, 31); + shift = MAX(shift, 0); + tab4[0] = 0; + tab4[1] = tab16[1] >> shift; + tab4[2] = tab16[2] >> shift; + tab4[3] = tab16[3] >> shift; + + do { + sx = *inbuf++; + x = sx & 0x7fffffff; /* sx = sign|mag */ + + if (x < 4) { + y = tab4[x]; + + } else if (x < 16) { + y = tab16[x]; + y = (scalei < 0) ? y << -scalei : y >> scalei; + + } else { + if (x < 64) { + y = pow43[x - 16]; + + /* fractional scale */ + y = MULSHIFT32(y, scalef); + shift = scalei - 3; + + } else { + /* normalize to [0x40000000, 0x7fffffff] */ + x <<= 17; + shift = 0; + if (x < 0x08000000) + x <<= 4, shift += 4; + if (x < 0x20000000) + x <<= 2, shift += 2; + if (x < 0x40000000) + x <<= 1, shift += 1; + + coef = (x < SQRTHALF) ? poly43lo : poly43hi; + + /* polynomial */ + y = coef[0]; + y = MULSHIFT32(y, x) + coef[1]; + y = MULSHIFT32(y, x) + coef[2]; + y = MULSHIFT32(y, x) + coef[3]; + y = MULSHIFT32(y, x) + coef[4]; + y = MULSHIFT32(y, pow2frac[shift]) << 3; + + /* fractional scale */ + y = MULSHIFT32(y, scalef); + shift = scalei - pow2exp[shift]; + } + + /* integer scale */ + if (shift < 0) { + shift = -shift; + if (y > (0x7fffffff >> shift)) + y = 0x7fffffff; /* clip */ + else + y <<= shift; + } else { + y >>= shift; + } + } + + /* sign and store */ + mask |= y; + *outbuf++ = (sx < 0) ? -y : y; + + } while (--num); + + return mask; +} + +/************************************************************************************** + * Function: DequantChannel + * + * Description: dequantize one granule, one channel worth of decoded Huffman + *codewords + * + * Inputs: sample buffer (decoded Huffman codewords), length = MAX_NSAMP + *samples work buffer for reordering short-block, length = MAX_REORDER_SAMPS + * samples (3 * width of largest short-block critical band) + * non-zero bound for this channel/granule + * valid FrameHeader, SideInfoSub, ScaleFactorInfoSub, and + *CriticalBandInfo structures for this channel/granule + * + * Outputs: MAX_NSAMP dequantized samples in sampleBuf + * updated non-zero bound (indicating which samples are != 0 after + *DQ) filled-in cbi structure indicating start and end critical bands + * + * Return: minimum number of guard bits in dequantized sampleBuf + * + * Notes: dequantized samples in Q(DQ_FRACBITS_OUT) format + **************************************************************************************/ +int DequantChannel(int *sampleBuf, int *workBuf, int *nonZeroBound, FrameHeader *fh, SideInfoSub *sis, + ScaleFactorInfoSub *sfis, CriticalBandInfo *cbi) { + int i, j, w, cb; + int cbStartL, cbEndL, cbStartS, cbEndS; + int nSamps, nonZero, sfactMultiplier, gbMask; + int globalGain, gainI; + int cbMax[3]; + ARRAY3 *buf; /* short block reorder */ + + /* set default start/end points for short/long blocks - will update with + * non-zero cb info */ + if (sis->blockType == 2) { + cbStartL = 0; + if (sis->mixedBlock) { + cbEndL = (fh->ver == MPEG1 ? 8 : 6); + cbStartS = 3; + } else { + cbEndL = 0; + cbStartS = 0; + } + cbEndS = 13; + } else { + /* long block */ + cbStartL = 0; + cbEndL = 22; + cbStartS = 13; + cbEndS = 13; + } + cbMax[2] = cbMax[1] = cbMax[0] = 0; + gbMask = 0; + i = 0; + + /* sfactScale = 0 --> quantizer step size = 2 + * sfactScale = 1 --> quantizer step size = sqrt(2) + * so sfactMultiplier = 2 or 4 (jump through globalGain by powers of 2 or + * sqrt(2)) + */ + sfactMultiplier = 2 * (sis->sfactScale + 1); + + /* offset globalGain by -2 if midSide enabled, for 1/sqrt(2) used in + * MidSideProc() (DequantBlock() does 0.25 * gainI so knocking it down by two + * is the same as dividing every sample by sqrt(2) = multiplying by 2^-.5) + */ + globalGain = sis->globalGain; + if (fh->modeExt >> 1) + globalGain -= 2; + globalGain += IMDCT_SCALE; /* scale everything by sqrt(2), for fast IMDCT36 */ + + /* long blocks */ + for (cb = 0; cb < cbEndL; cb++) { + nonZero = 0; + nSamps = fh->sfBand->l[cb + 1] - fh->sfBand->l[cb]; + gainI = 210 - globalGain + sfactMultiplier * (sfis->l[cb] + (sis->preFlag ? (int) preTab[cb] : 0)); + + nonZero |= DequantBlock(sampleBuf + i, sampleBuf + i, nSamps, gainI); + i += nSamps; + + /* update highest non-zero critical band */ + if (nonZero) + cbMax[0] = cb; + gbMask |= nonZero; + + if (i >= *nonZeroBound) + break; + } + + /* set cbi (Type, EndS[], EndSMax will be overwritten if we proceed to do + * short blocks) */ + cbi->cbType = 0; /* long only */ + cbi->cbEndL = cbMax[0]; + cbi->cbEndS[0] = cbi->cbEndS[1] = cbi->cbEndS[2] = 0; + cbi->cbEndSMax = 0; + + /* early exit if no short blocks */ + if (cbStartS >= 12) + return CLZ(gbMask) - 1; + + /* short blocks */ + cbMax[2] = cbMax[1] = cbMax[0] = cbStartS; + for (cb = cbStartS; cb < cbEndS; cb++) { + nSamps = fh->sfBand->s[cb + 1] - fh->sfBand->s[cb]; + for (w = 0; w < 3; w++) { + nonZero = 0; + gainI = 210 - globalGain + 8 * sis->subBlockGain[w] + sfactMultiplier * (sfis->s[cb][w]); + + nonZero |= DequantBlock(sampleBuf + i + nSamps * w, workBuf + nSamps * w, nSamps, gainI); + + /* update highest non-zero critical band */ + if (nonZero) + cbMax[w] = cb; + gbMask |= nonZero; + } + + /* reorder blocks */ + buf = (ARRAY3 *) (sampleBuf + i); + i += 3 * nSamps; + for (j = 0; j < nSamps; j++) { + buf[j][0] = workBuf[0 * nSamps + j]; + buf[j][1] = workBuf[1 * nSamps + j]; + buf[j][2] = workBuf[2 * nSamps + j]; + } + + ASSERT(3 * nSamps <= MAX_REORDER_SAMPS); + + if (i >= *nonZeroBound) + break; + } + + /* i = last non-zero INPUT sample processed, which corresponds to highest + * possible non-zero OUTPUT sample (after reorder) however, the original nzb + * is no longer necessarily true for each cb, buf[][] is updated with 3*nSamps + * samples (i increases 3*nSamps each time) (buf[j + 1][0] = 3 (input) samples + * ahead of buf[j][0]) so update nonZeroBound to i + */ + *nonZeroBound = i; + + ASSERT(*nonZeroBound <= MAX_NSAMP); + + cbi->cbType = (sis->mixedBlock ? 2 : 1); /* 2 = mixed short/long, 1 = short only */ + + cbi->cbEndS[0] = cbMax[0]; + cbi->cbEndS[1] = cbMax[1]; + cbi->cbEndS[2] = cbMax[2]; + + cbi->cbEndSMax = cbMax[0]; + cbi->cbEndSMax = MAX(cbi->cbEndSMax, cbMax[1]); + cbi->cbEndSMax = MAX(cbi->cbEndSMax, cbMax[2]); + + return CLZ(gbMask) - 1; +} + +/* input to Polyphase = Q(DQ_FRACBITS_OUT-2), gain 2 bits in convolution + * we also have the implicit bias of 2^15 to add back, so net fraction bits = + * DQ_FRACBITS_OUT - 2 - 2 - 15 + * (see comment on Dequantize() for more info) + */ +#define DEF_NFRACBITS (DQ_FRACBITS_OUT - 2 - 2 - 15) +#define CSHIFT \ + 12 /* coefficients have 12 leading sign bits for early-terminating \ + mulitplies */ + +static __inline short ClipToShort(int x, int fracBits) { + int sign; + + /* assumes you've already rounded (x += (1 << (fracBits-1))) */ + x >>= fracBits; + + /* Ken's trick: clips to [-32768, 32767] */ + sign = x >> 31; + if (sign != (x >> 15)) + x = sign ^ ((1 << 15) - 1); + + return (short) x; +} + +#define MC0M(x) \ + { \ + c1 = *coef; \ + coef++; \ + c2 = *coef; \ + coef++; \ + vLo = *(vb1 + (x)); \ + vHi = *(vb1 + (23 - (x))); \ + sum1L = MADD64(sum1L, vLo, c1); \ + sum1L = MADD64(sum1L, vHi, -c2); \ + } + +#define MC1M(x) \ + { \ + c1 = *coef; \ + coef++; \ + vLo = *(vb1 + (x)); \ + sum1L = MADD64(sum1L, vLo, c1); \ + } + +#define MC2M(x) \ + { \ + c1 = *coef; \ + coef++; \ + c2 = *coef; \ + coef++; \ + vLo = *(vb1 + (x)); \ + vHi = *(vb1 + (23 - (x))); \ + sum1L = MADD64(sum1L, vLo, c1); \ + sum2L = MADD64(sum2L, vLo, c2); \ + sum1L = MADD64(sum1L, vHi, -c2); \ + sum2L = MADD64(sum2L, vHi, c1); \ + } + +/************************************************************************************** + * Function: PolyphaseMono + * + * Description: filter one subband and produce 32 output PCM samples for one + *channel + * + * Inputs: pointer to PCM output buffer + * number of "extra shifts" (vbuf format = Q(DQ_FRACBITS_OUT-2)) + * pointer to start of vbuf (preserved from last call) + * start of filter coefficient table (in proper, shuffled order) + * no minimum number of guard bits is required for input vbuf + * (see additional scaling comments below) + * + * Outputs: 32 samples of one channel of decoded PCM data, (i.e. Q16.0) + * + * Return: none + * + * TODO: add 32-bit version for platforms where 64-bit mul-acc is not + *supported (note max filter gain - see polyCoef[] comments) + **************************************************************************************/ +void PolyphaseMono(short *pcm, int *vbuf, const uint32_t *coefBase) { + int i; + const uint32_t *coef; + int *vb1; + int vLo, vHi, c1, c2; + Word64 sum1L, sum2L, rndVal; + + rndVal = (Word64) (1 << (DEF_NFRACBITS - 1 + (32 - CSHIFT))); + + /* special case, output sample 0 */ + coef = coefBase; + vb1 = vbuf; + sum1L = rndVal; + + MC0M(0) + MC0M(1) + MC0M(2) + MC0M(3) + MC0M(4) + MC0M(5) + MC0M(6) + MC0M(7) + + *(pcm + 0) = ClipToShort((int) SAR64(sum1L, (32 - CSHIFT)), DEF_NFRACBITS); + + /* special case, output sample 16 */ + coef = coefBase + 256; + vb1 = vbuf + 64 * 16; + sum1L = rndVal; + + MC1M(0) + MC1M(1) + MC1M(2) + MC1M(3) + MC1M(4) + MC1M(5) + MC1M(6) + MC1M(7) + + *(pcm + 16) = ClipToShort((int) SAR64(sum1L, (32 - CSHIFT)), DEF_NFRACBITS); + + /* main convolution loop: sum1L = samples 1, 2, 3, ... 15 sum2L = samples + * 31, 30, ... 17 */ + coef = coefBase + 16; + vb1 = vbuf + 64; + pcm++; + + /* right now, the compiler creates bad asm from this... */ + for (i = 15; i > 0; i--) { + sum1L = sum2L = rndVal; + + MC2M(0) + MC2M(1) + MC2M(2) + MC2M(3) + MC2M(4) + MC2M(5) + MC2M(6) + MC2M(7) + + vb1 += 64; + *(pcm) = ClipToShort((int) SAR64(sum1L, (32 - CSHIFT)), DEF_NFRACBITS); + *(pcm + 2 * i) = ClipToShort((int) SAR64(sum2L, (32 - CSHIFT)), DEF_NFRACBITS); + pcm++; + } +} + +#define MC0S(x) \ + { \ + c1 = *coef; \ + coef++; \ + c2 = *coef; \ + coef++; \ + vLo = *(vb1 + (x)); \ + vHi = *(vb1 + (23 - (x))); \ + sum1L = MADD64(sum1L, vLo, c1); \ + sum1L = MADD64(sum1L, vHi, -c2); \ + vLo = *(vb1 + 32 + (x)); \ + vHi = *(vb1 + 32 + (23 - (x))); \ + sum1R = MADD64(sum1R, vLo, c1); \ + sum1R = MADD64(sum1R, vHi, -c2); \ + } + +#define MC1S(x) \ + { \ + c1 = *coef; \ + coef++; \ + vLo = *(vb1 + (x)); \ + sum1L = MADD64(sum1L, vLo, c1); \ + vLo = *(vb1 + 32 + (x)); \ + sum1R = MADD64(sum1R, vLo, c1); \ + } + +#define MC2S(x) \ + { \ + c1 = *coef; \ + coef++; \ + c2 = *coef; \ + coef++; \ + vLo = *(vb1 + (x)); \ + vHi = *(vb1 + (23 - (x))); \ + sum1L = MADD64(sum1L, vLo, c1); \ + sum2L = MADD64(sum2L, vLo, c2); \ + sum1L = MADD64(sum1L, vHi, -c2); \ + sum2L = MADD64(sum2L, vHi, c1); \ + vLo = *(vb1 + 32 + (x)); \ + vHi = *(vb1 + 32 + (23 - (x))); \ + sum1R = MADD64(sum1R, vLo, c1); \ + sum2R = MADD64(sum2R, vLo, c2); \ + sum1R = MADD64(sum1R, vHi, -c2); \ + sum2R = MADD64(sum2R, vHi, c1); \ + } + +/************************************************************************************** + * Function: PolyphaseStereo + * + * Description: filter one subband and produce 32 output PCM samples for each + *channel + * + * Inputs: pointer to PCM output buffer + * number of "extra shifts" (vbuf format = Q(DQ_FRACBITS_OUT-2)) + * pointer to start of vbuf (preserved from last call) + * start of filter coefficient table (in proper, shuffled order) + * no minimum number of guard bits is required for input vbuf + * (see additional scaling comments below) + * + * Outputs: 32 samples of two channels of decoded PCM data, (i.e. Q16.0) + * + * Return: none + * + * Notes: interleaves PCM samples LRLRLR... + * + * TODO: add 32-bit version for platforms where 64-bit mul-acc is not + *supported + **************************************************************************************/ +void PolyphaseStereo(short *pcm, int *vbuf, const uint32_t *coefBase) { + int i; + const uint32_t *coef; + int *vb1; + int vLo, vHi, c1, c2; + Word64 sum1L, sum2L, sum1R, sum2R, rndVal; + + rndVal = (Word64) (1 << (DEF_NFRACBITS - 1 + (32 - CSHIFT))); + + /* special case, output sample 0 */ + coef = coefBase; + vb1 = vbuf; + sum1L = sum1R = rndVal; + + MC0S(0) + MC0S(1) + MC0S(2) + MC0S(3) + MC0S(4) + MC0S(5) + MC0S(6) + MC0S(7) + + *(pcm + 0) = ClipToShort((int) SAR64(sum1L, (32 - CSHIFT)), DEF_NFRACBITS); + *(pcm + 1) = ClipToShort((int) SAR64(sum1R, (32 - CSHIFT)), DEF_NFRACBITS); + + /* special case, output sample 16 */ + coef = coefBase + 256; + vb1 = vbuf + 64 * 16; + sum1L = sum1R = rndVal; + + MC1S(0) + MC1S(1) + MC1S(2) + MC1S(3) + MC1S(4) + MC1S(5) + MC1S(6) + MC1S(7) + + *(pcm + 2 * 16 + 0) = ClipToShort((int) SAR64(sum1L, (32 - CSHIFT)), DEF_NFRACBITS); + *(pcm + 2 * 16 + 1) = ClipToShort((int) SAR64(sum1R, (32 - CSHIFT)), DEF_NFRACBITS); + + /* main convolution loop: sum1L = samples 1, 2, 3, ... 15 sum2L = samples + * 31, 30, ... 17 */ + coef = coefBase + 16; + vb1 = vbuf + 64; + pcm += 2; + + /* right now, the compiler creates bad asm from this... */ + for (i = 15; i > 0; i--) { + sum1L = sum2L = rndVal; + sum1R = sum2R = rndVal; + + MC2S(0) + MC2S(1) + MC2S(2) + MC2S(3) + MC2S(4) + MC2S(5) + MC2S(6) + MC2S(7) + + vb1 += 64; + *(pcm + 0) = ClipToShort((int) SAR64(sum1L, (32 - CSHIFT)), DEF_NFRACBITS); + *(pcm + 1) = ClipToShort((int) SAR64(sum1R, (32 - CSHIFT)), DEF_NFRACBITS); + *(pcm + 2 * 2 * i + 0) = ClipToShort((int) SAR64(sum2L, (32 - CSHIFT)), DEF_NFRACBITS); + *(pcm + 2 * 2 * i + 1) = ClipToShort((int) SAR64(sum2R, (32 - CSHIFT)), DEF_NFRACBITS); + pcm += 2; + } +} + +/************************************************************************************** + * Function: Subband + * + * Description: do subband transform on all the blocks in one granule, all + *channels + * + * Inputs: filled MP3DecInfo structure, after calling IMDCT for all + *channels vbuf[ch] and vindex[ch] must be preserved between calls + * + * Outputs: decoded PCM data, interleaved LRLRLR... if stereo + * + * Return: 0 on success, -1 if null input pointers + **************************************************************************************/ +int Subband(MP3DecInfo *mp3DecInfo, short *pcmBuf) { + int b; + HuffmanInfo *hi; + IMDCTInfo *mi; + SubbandInfo *sbi; + + /* validate pointers */ + if (!mp3DecInfo || !mp3DecInfo->HuffmanInfoPS || !mp3DecInfo->IMDCTInfoPS || !mp3DecInfo->SubbandInfoPS) + return -1; + + hi = (HuffmanInfo *) mp3DecInfo->HuffmanInfoPS; + mi = (IMDCTInfo *) (mp3DecInfo->IMDCTInfoPS); + sbi = (SubbandInfo *) (mp3DecInfo->SubbandInfoPS); + + if (mp3DecInfo->nChans == 2) { + /* stereo */ + for (b = 0; b < BLOCK_SIZE; b++) { + FDCT32(mi->outBuf[0][b], sbi->vbuf + 0 * 32, sbi->vindex, (b & 0x01), mi->gb[0]); + FDCT32(mi->outBuf[1][b], sbi->vbuf + 1 * 32, sbi->vindex, (b & 0x01), mi->gb[1]); + PolyphaseStereo(pcmBuf, sbi->vbuf + sbi->vindex + VBUF_LENGTH * (b & 0x01), polyCoef); + sbi->vindex = (sbi->vindex - (b & 0x01)) & 7; + pcmBuf += (2 * NBANDS); + } + } else { + /* mono */ + for (b = 0; b < BLOCK_SIZE; b++) { + FDCT32(mi->outBuf[0][b], sbi->vbuf + 0 * 32, sbi->vindex, (b & 0x01), mi->gb[0]); + PolyphaseMono(pcmBuf, sbi->vbuf + sbi->vindex + VBUF_LENGTH * (b & 0x01), polyCoef); + sbi->vindex = (sbi->vindex - (b & 0x01)) & 7; + pcmBuf += NBANDS; + } + } + + return 0; +} + +/************************************************************************************** + * Function: MidSideProc + * + * Description: sum-difference stereo reconstruction + * + * Inputs: vector x with dequantized samples from left and right channels + * number of non-zero samples (MAX of left and right) + * assume 1 guard bit in input + * guard bit mask (left and right channels) + * + * Outputs: updated sample vector x + * updated guard bit mask + * + * Return: none + * + * Notes: assume at least 1 GB in input + **************************************************************************************/ +void MidSideProc(int x[MAX_NCHAN][MAX_NSAMP], int nSamps, int mOut[2]) { + int i, xr, xl, mOutL, mOutR; + + /* L = (M+S)/sqrt(2), R = (M-S)/sqrt(2) + * NOTE: 1/sqrt(2) done in DequantChannel() - see comments there + */ + mOutL = mOutR = 0; + for (i = 0; i < nSamps; i++) { + xl = x[0][i]; + xr = x[1][i]; + x[0][i] = xl + xr; + x[1][i] = xl - xr; + mOutL |= FASTABS(x[0][i]); + mOutR |= FASTABS(x[1][i]); + } + mOut[0] |= mOutL; + mOut[1] |= mOutR; +} + +/************************************************************************************** + * Function: IntensityProcMPEG1 + * + * Description: intensity stereo processing for MPEG1 + * + * Inputs: vector x with dequantized samples from left and right channels + * number of non-zero samples in left channel + * valid FrameHeader struct + * two each of ScaleFactorInfoSub, CriticalBandInfo structs (both + *channels) flags indicating midSide on/off, mixedBlock on/off guard bit mask + *(left and right channels) + * + * Outputs: updated sample vector x + * updated guard bit mask + * + * Return: none + * + * Notes: assume at least 1 GB in input + * + * TODO: combine MPEG1/2 into one function (maybe) + * make sure all the mixed-block and IIP logic is right + **************************************************************************************/ +void IntensityProcMPEG1(int x[MAX_NCHAN][MAX_NSAMP], int nSamps, FrameHeader *fh, ScaleFactorInfoSub *sfis, + CriticalBandInfo *cbi, int midSideFlag, int mixFlag, int mOut[2]) { + int i = 0, j = 0, n = 0, cb = 0, w = 0; + int sampsLeft, isf, mOutL, mOutR, xl, xr; + int fl, fr, fls[3], frs[3]; + int cbStartL = 0, cbStartS = 0, cbEndL = 0, cbEndS = 0; + int *isfTab; + + /* NOTE - this works fine for mixed blocks, as long as the switch point starts + * in the short block section (i.e. on or after sample 36 = sfBand->l[8] = + * 3*sfBand->s[3] is this a safe assumption? + * TODO - intensity + mixed not quite right (diff = 11 on he_mode) + * figure out correct implementation (spec ambiguous about when to do short + * block reorder) + */ + if (cbi[1].cbType == 0) { + /* long block */ + cbStartL = cbi[1].cbEndL + 1; + cbEndL = cbi[0].cbEndL + 1; + cbStartS = cbEndS = 0; + i = fh->sfBand->l[cbStartL]; + } else if (cbi[1].cbType == 1 || cbi[1].cbType == 2) { + /* short or mixed block */ + cbStartS = cbi[1].cbEndSMax + 1; + cbEndS = cbi[0].cbEndSMax + 1; + cbStartL = cbEndL = 0; + i = 3 * fh->sfBand->s[cbStartS]; + } + + sampsLeft = nSamps - i; /* process to length of left */ + isfTab = (int *) ISFMpeg1[midSideFlag]; + mOutL = mOutR = 0; + + /* long blocks */ + for (cb = cbStartL; cb < cbEndL && sampsLeft > 0; cb++) { + isf = sfis->l[cb]; + if (isf == 7) { + fl = ISFIIP[midSideFlag][0]; + fr = ISFIIP[midSideFlag][1]; + } else { + fl = isfTab[isf]; + fr = isfTab[6] - isfTab[isf]; + } + + n = fh->sfBand->l[cb + 1] - fh->sfBand->l[cb]; + for (j = 0; j < n && sampsLeft > 0; j++, i++) { + xr = MULSHIFT32(fr, x[0][i]) << 2; + x[1][i] = xr; + mOutR |= FASTABS(xr); + xl = MULSHIFT32(fl, x[0][i]) << 2; + x[0][i] = xl; + mOutL |= FASTABS(xl); + sampsLeft--; + } + } + + /* short blocks */ + for (cb = cbStartS; cb < cbEndS && sampsLeft >= 3; cb++) { + for (w = 0; w < 3; w++) { + isf = sfis->s[cb][w]; + if (isf == 7) { + fls[w] = ISFIIP[midSideFlag][0]; + frs[w] = ISFIIP[midSideFlag][1]; + } else { + fls[w] = isfTab[isf]; + frs[w] = isfTab[6] - isfTab[isf]; + } + } + + n = fh->sfBand->s[cb + 1] - fh->sfBand->s[cb]; + for (j = 0; j < n && sampsLeft >= 3; j++, i += 3) { + xr = MULSHIFT32(frs[0], x[0][i + 0]) << 2; + x[1][i + 0] = xr; + mOutR |= FASTABS(xr); + xl = MULSHIFT32(fls[0], x[0][i + 0]) << 2; + x[0][i + 0] = xl; + mOutL |= FASTABS(xl); + xr = MULSHIFT32(frs[1], x[0][i + 1]) << 2; + x[1][i + 1] = xr; + mOutR |= FASTABS(xr); + xl = MULSHIFT32(fls[1], x[0][i + 1]) << 2; + x[0][i + 1] = xl; + mOutL |= FASTABS(xl); + xr = MULSHIFT32(frs[2], x[0][i + 2]) << 2; + x[1][i + 2] = xr; + mOutR |= FASTABS(xr); + xl = MULSHIFT32(fls[2], x[0][i + 2]) << 2; + x[0][i + 2] = xl; + mOutL |= FASTABS(xl); + sampsLeft -= 3; + } + } + mOut[0] = mOutL; + mOut[1] = mOutR; + + return; +} + +/************************************************************************************** + * Function: IntensityProcMPEG2 + * + * Description: intensity stereo processing for MPEG2 + * + * Inputs: vector x with dequantized samples from left and right channels + * number of non-zero samples in left channel + * valid FrameHeader struct + * two each of ScaleFactorInfoSub, CriticalBandInfo structs (both + *channels) ScaleFactorJS struct with joint stereo info from UnpackSFMPEG2() + * flags indicating midSide on/off, mixedBlock on/off + * guard bit mask (left and right channels) + * + * Outputs: updated sample vector x + * updated guard bit mask + * + * Return: none + * + * Notes: assume at least 1 GB in input + * + * TODO: combine MPEG1/2 into one function (maybe) + * make sure all the mixed-block and IIP logic is right + * probably redo IIP logic to be simpler + **************************************************************************************/ +void IntensityProcMPEG2(int x[MAX_NCHAN][MAX_NSAMP], int nSamps, FrameHeader *fh, ScaleFactorInfoSub *sfis, + CriticalBandInfo *cbi, ScaleFactorJS *sfjs, int midSideFlag, int mixFlag, int mOut[2]) { + int i, j, k, n, r, cb, w; + int fl, fr, mOutL, mOutR, xl, xr; + int sampsLeft; + int isf, sfIdx, tmp, il[23]; + int *isfTab; + int cbStartL, cbStartS, cbEndL, cbEndS; + + isfTab = (int *) ISFMpeg2[sfjs->intensityScale][midSideFlag]; + mOutL = mOutR = 0; + + /* fill buffer with illegal intensity positions (depending on slen) */ + for (k = r = 0; r < 4; r++) { + tmp = (1 << sfjs->slen[r]) - 1; + for (j = 0; j < sfjs->nr[r]; j++, k++) + il[k] = tmp; + } + + if (cbi[1].cbType == 0) { + /* long blocks */ + il[21] = il[22] = 1; + cbStartL = cbi[1].cbEndL + 1; /* start at end of right */ + cbEndL = cbi[0].cbEndL + 1; /* process to end of left */ + i = fh->sfBand->l[cbStartL]; + sampsLeft = nSamps - i; + + for (cb = cbStartL; cb < cbEndL; cb++) { + sfIdx = sfis->l[cb]; + if (sfIdx == il[cb]) { + fl = ISFIIP[midSideFlag][0]; + fr = ISFIIP[midSideFlag][1]; + } else { + isf = (sfis->l[cb] + 1) >> 1; + fl = isfTab[(sfIdx & 0x01 ? isf : 0)]; + fr = isfTab[(sfIdx & 0x01 ? 0 : isf)]; + } + n = MIN(fh->sfBand->l[cb + 1] - fh->sfBand->l[cb], sampsLeft); + + for (j = 0; j < n; j++, i++) { + xr = MULSHIFT32(fr, x[0][i]) << 2; + x[1][i] = xr; + mOutR |= FASTABS(xr); + xl = MULSHIFT32(fl, x[0][i]) << 2; + x[0][i] = xl; + mOutL |= FASTABS(xl); + } + + /* early exit once we've used all the non-zero samples */ + sampsLeft -= n; + if (sampsLeft == 0) + break; + } + } else { + /* short or mixed blocks */ + il[12] = 1; + + for (w = 0; w < 3; w++) { + cbStartS = cbi[1].cbEndS[w] + 1; /* start at end of right */ + cbEndS = cbi[0].cbEndS[w] + 1; /* process to end of left */ + i = 3 * fh->sfBand->s[cbStartS] + w; + + /* skip through sample array by 3, so early-exit logic would be more + * tricky */ + for (cb = cbStartS; cb < cbEndS; cb++) { + sfIdx = sfis->s[cb][w]; + if (sfIdx == il[cb]) { + fl = ISFIIP[midSideFlag][0]; + fr = ISFIIP[midSideFlag][1]; + } else { + isf = (sfis->s[cb][w] + 1) >> 1; + fl = isfTab[(sfIdx & 0x01 ? isf : 0)]; + fr = isfTab[(sfIdx & 0x01 ? 0 : isf)]; + } + n = fh->sfBand->s[cb + 1] - fh->sfBand->s[cb]; + + for (j = 0; j < n; j++, i += 3) { + xr = MULSHIFT32(fr, x[0][i]) << 2; + x[1][i] = xr; + mOutR |= FASTABS(xr); + xl = MULSHIFT32(fl, x[0][i]) << 2; + x[0][i] = xl; + mOutL |= FASTABS(xl); + } + } + } + } + mOut[0] = mOutL; + mOut[1] = mOutR; + + return; +} + +/* scale factor lengths (num bits) */ +static const char SFLenTab[16][2] = { + {0, 0}, {0, 1}, {0, 2}, {0, 3}, {3, 0}, {1, 1}, {1, 2}, {1, 3}, + {2, 1}, {2, 2}, {2, 3}, {3, 1}, {3, 2}, {3, 3}, {4, 2}, {4, 3}, +}; + +/************************************************************************************** + * Function: UnpackSFMPEG1 + * + * Description: unpack MPEG 1 scalefactors from bitstream + * + * Inputs: BitStreamInfo, SideInfoSub, ScaleFactorInfoSub structs for this + * granule/channel + * vector of scfsi flags from side info, length = 4 (MAX_SCFBD) + * index of current granule + * ScaleFactorInfoSub from granule 0 (for granule 1, if scfsi[i] is + *set, then we just replicate the scale factors from granule 0 in the i'th set + *of scalefactor bands) + * + * Outputs: updated BitStreamInfo struct + * scalefactors in sfis (short and/or long arrays, as appropriate) + * + * Return: none + * + * Notes: set order of short blocks to s[band][window] instead of + *s[window][band] so that we index through consectutive memory locations when + *unpacking (make sure dequantizer follows same convention) Illegal Intensity + *Position = 7 (always) for MPEG1 scale factors + **************************************************************************************/ +static void UnpackSFMPEG1(BitStreamInfo *bsi, SideInfoSub *sis, ScaleFactorInfoSub *sfis, int *scfsi, int gr, + ScaleFactorInfoSub *sfisGr0) { + int sfb; + int slen0, slen1; + + /* these can be 0, so make sure GetBits(bsi, 0) returns 0 (no >> 32 or + * anything) */ + slen0 = (int) SFLenTab[sis->sfCompress][0]; + slen1 = (int) SFLenTab[sis->sfCompress][1]; + + if (sis->blockType == 2) { + /* short block, type 2 (implies winSwitchFlag == 1) */ + if (sis->mixedBlock) { + /* do long block portion */ + for (sfb = 0; sfb < 8; sfb++) + sfis->l[sfb] = (char) GetBits(bsi, slen0); + sfb = 3; + } else { + /* all short blocks */ + sfb = 0; + } + + for (; sfb < 6; sfb++) { + sfis->s[sfb][0] = (char) GetBits(bsi, slen0); + sfis->s[sfb][1] = (char) GetBits(bsi, slen0); + sfis->s[sfb][2] = (char) GetBits(bsi, slen0); + } + + for (; sfb < 12; sfb++) { + sfis->s[sfb][0] = (char) GetBits(bsi, slen1); + sfis->s[sfb][1] = (char) GetBits(bsi, slen1); + sfis->s[sfb][2] = (char) GetBits(bsi, slen1); + } + + /* last sf band not transmitted */ + sfis->s[12][0] = sfis->s[12][1] = sfis->s[12][2] = 0; + } else { + /* long blocks, type 0, 1, or 3 */ + if (gr == 0) { + /* first granule */ + for (sfb = 0; sfb < 11; sfb++) + sfis->l[sfb] = (char) GetBits(bsi, slen0); + for (sfb = 11; sfb < 21; sfb++) + sfis->l[sfb] = (char) GetBits(bsi, slen1); + return; + } else { + /* second granule + * scfsi: 0 = different scalefactors for each granule, 1 = copy sf's from + * granule 0 into granule 1 for block type == 2, scfsi is always 0 + */ + sfb = 0; + if (scfsi[0]) + for (; sfb < 6; sfb++) + sfis->l[sfb] = sfisGr0->l[sfb]; + else + for (; sfb < 6; sfb++) + sfis->l[sfb] = (char) GetBits(bsi, slen0); + if (scfsi[1]) + for (; sfb < 11; sfb++) + sfis->l[sfb] = sfisGr0->l[sfb]; + else + for (; sfb < 11; sfb++) + sfis->l[sfb] = (char) GetBits(bsi, slen0); + if (scfsi[2]) + for (; sfb < 16; sfb++) + sfis->l[sfb] = sfisGr0->l[sfb]; + else + for (; sfb < 16; sfb++) + sfis->l[sfb] = (char) GetBits(bsi, slen1); + if (scfsi[3]) + for (; sfb < 21; sfb++) + sfis->l[sfb] = sfisGr0->l[sfb]; + else + for (; sfb < 21; sfb++) + sfis->l[sfb] = (char) GetBits(bsi, slen1); + } + /* last sf band not transmitted */ + sfis->l[21] = 0; + sfis->l[22] = 0; + } +} + +/* NRTab[size + 3*is_right][block type][partition] + * block type index: 0 = (bt0,bt1,bt3), 1 = bt2 non-mixed, 2 = bt2 mixed + * partition: scale factor groups (sfb1 through sfb4) + * for block type = 2 (mixed or non-mixed) / by 3 is rolled into this table + * (for 3 short blocks per long block) + * see 2.4.3.2 in MPEG 2 (low sample rate) spec + * stuff rolled into this table: + * NRTab[x][1][y] --> (NRTab[x][1][y]) / 3 + * NRTab[x][2][>=1] --> (NRTab[x][2][>=1]) / 3 (first partition is long + * block) + */ +static const char NRTab[6][3][4] = { + /* non-intensity stereo */ + { + {6, 5, 5, 5}, + {3, 3, 3, 3}, /* includes / 3 */ + {6, 3, 3, 3}, /* includes / 3 except for first entry */ + }, + { + {6, 5, 7, 3}, + {3, 3, 4, 2}, + {6, 3, 4, 2}, + }, + { + {11, 10, 0, 0}, {6, 6, 0, 0}, {6, 3, 6, 0}, /* spec = [15,18,0,0], but 15 = 6L + 9S, so move 9/3=3 into col 1, + 18/3=6 into col 2 and adj. slen[1,2] below */ + }, + /* intensity stereo, right chan */ + { + {7, 7, 7, 0}, + {4, 4, 4, 0}, + {6, 5, 4, 0}, + }, + { + {6, 6, 6, 3}, + {4, 3, 3, 2}, + {6, 4, 3, 2}, + }, + { + {8, 8, 5, 0}, + {5, 4, 3, 0}, + {6, 6, 3, 0}, + }}; + +/************************************************************************************** + * Function: UnpackSFMPEG2 + * + * Description: unpack MPEG 2 scalefactors from bitstream + * + * Inputs: BitStreamInfo, SideInfoSub, ScaleFactorInfoSub structs for this + * granule/channel + * index of current granule and channel + * ScaleFactorInfoSub from this granule + * modeExt field from frame header, to tell whether intensity + *stereo is on ScaleFactorJS struct for storing IIP info used in Dequant() + * + * Outputs: updated BitStreamInfo struct + * scalefactors in sfis (short and/or long arrays, as appropriate) + * updated intensityScale and preFlag flags + * + * Return: none + * + * Notes: Illegal Intensity Position = (2^slen) - 1 for MPEG2 scale + *factors + * + * TODO: optimize the / and % stuff (only do one divide, get modulo x + * with (x / m) * m, etc.) + **************************************************************************************/ +static void UnpackSFMPEG2(BitStreamInfo *bsi, SideInfoSub *sis, ScaleFactorInfoSub *sfis, int gr, int ch, int modeExt, + ScaleFactorJS *sfjs) { + int i, sfb, sfcIdx, btIdx, nrIdx, iipTest; + int slen[4], nr[4]; + int sfCompress, preFlag, intensityScale; + + sfCompress = sis->sfCompress; + preFlag = 0; + intensityScale = 0; + + /* stereo mode bits (1 = on): bit 1 = mid-side on/off, bit 0 = intensity + * on/off */ + if (!((modeExt & 0x01) && (ch == 1))) { + /* in other words: if ((modeExt & 0x01) == 0 || ch == 0) */ + if (sfCompress < 400) { + /* max slen = floor[(399/16) / 5] = 4 */ + slen[0] = (sfCompress >> 4) / 5; + slen[1] = (sfCompress >> 4) % 5; + slen[2] = (sfCompress & 0x0f) >> 2; + slen[3] = (sfCompress & 0x03); + sfcIdx = 0; + } else if (sfCompress < 500) { + /* max slen = floor[(99/4) / 5] = 4 */ + sfCompress -= 400; + slen[0] = (sfCompress >> 2) / 5; + slen[1] = (sfCompress >> 2) % 5; + slen[2] = (sfCompress & 0x03); + slen[3] = 0; + sfcIdx = 1; + } else { + /* max slen = floor[11/3] = 3 (sfCompress = 9 bits in MPEG2) */ + sfCompress -= 500; + slen[0] = sfCompress / 3; + slen[1] = sfCompress % 3; + slen[2] = slen[3] = 0; + if (sis->mixedBlock) { + /* adjust for long/short mix logic (see comment above in NRTab[] + * definition) */ + slen[2] = slen[1]; + slen[1] = slen[0]; + } + preFlag = 1; + sfcIdx = 2; + } + } else { + /* intensity stereo ch = 1 (right) */ + intensityScale = sfCompress & 0x01; + sfCompress >>= 1; + if (sfCompress < 180) { + /* max slen = floor[35/6] = 5 (from mod 36) */ + slen[0] = (sfCompress / 36); + slen[1] = (sfCompress % 36) / 6; + slen[2] = (sfCompress % 36) % 6; + slen[3] = 0; + sfcIdx = 3; + } else if (sfCompress < 244) { + /* max slen = floor[63/16] = 3 */ + sfCompress -= 180; + slen[0] = (sfCompress & 0x3f) >> 4; + slen[1] = (sfCompress & 0x0f) >> 2; + slen[2] = (sfCompress & 0x03); + slen[3] = 0; + sfcIdx = 4; + } else { + /* max slen = floor[11/3] = 3 (max sfCompress >> 1 = 511/2 = 255) */ + sfCompress -= 244; + slen[0] = (sfCompress / 3); + slen[1] = (sfCompress % 3); + slen[2] = slen[3] = 0; + sfcIdx = 5; + } + } + + /* set index based on block type: (0,1,3) --> 0, (2 non-mixed) --> 1, (2 + * mixed) ---> 2 */ + btIdx = 0; + if (sis->blockType == 2) + btIdx = (sis->mixedBlock ? 2 : 1); + for (i = 0; i < 4; i++) + nr[i] = (int) NRTab[sfcIdx][btIdx][i]; + + /* save intensity stereo scale factor info */ + if ((modeExt & 0x01) && (ch == 1)) { + for (i = 0; i < 4; i++) { + sfjs->slen[i] = slen[i]; + sfjs->nr[i] = nr[i]; + } + sfjs->intensityScale = intensityScale; + } + sis->preFlag = preFlag; + + /* short blocks */ + if (sis->blockType == 2) { + if (sis->mixedBlock) { + /* do long block portion */ + iipTest = (1 << slen[0]) - 1; + for (sfb = 0; sfb < 6; sfb++) { + sfis->l[sfb] = (char) GetBits(bsi, slen[0]); + } + sfb = 3; /* start sfb for short */ + nrIdx = 1; + } else { + /* all short blocks, so start nr, sfb at 0 */ + sfb = 0; + nrIdx = 0; + } + + /* remaining short blocks, sfb just keeps incrementing */ + for (; nrIdx <= 3; nrIdx++) { + iipTest = (1 << slen[nrIdx]) - 1; + for (i = 0; i < nr[nrIdx]; i++, sfb++) { + sfis->s[sfb][0] = (char) GetBits(bsi, slen[nrIdx]); + sfis->s[sfb][1] = (char) GetBits(bsi, slen[nrIdx]); + sfis->s[sfb][2] = (char) GetBits(bsi, slen[nrIdx]); + } + } + /* last sf band not transmitted */ + sfis->s[12][0] = sfis->s[12][1] = sfis->s[12][2] = 0; + } else { + /* long blocks */ + sfb = 0; + for (nrIdx = 0; nrIdx <= 3; nrIdx++) { + iipTest = (1 << slen[nrIdx]) - 1; + for (i = 0; i < nr[nrIdx]; i++, sfb++) { + sfis->l[sfb] = (char) GetBits(bsi, slen[nrIdx]); + } + } + /* last sf band not transmitted */ + sfis->l[21] = sfis->l[22] = 0; + } +} + +/************************************************************************************** + * Function: UnpackScaleFactors + * + * Description: parse the fields of the MP3 scale factor data section + * + * Inputs: MP3DecInfo structure filled by UnpackFrameHeader() and + *UnpackSideInfo() buffer pointing to the MP3 scale factor data pointer to bit + *offset (0-7) indicating starting bit in buf[0] number of bits available in + *data buffer index of current granule and channel + * + * Outputs: updated platform-specific ScaleFactorInfo struct + * updated bitOffset + * + * Return: length (in bytes) of scale factor data, -1 if null input + *pointers + **************************************************************************************/ +int UnpackScaleFactors(MP3DecInfo *mp3DecInfo, unsigned char *buf, int *bitOffset, int bitsAvail, int gr, int ch) { + int bitsUsed; + unsigned char *startBuf; + BitStreamInfo bitStreamInfo, *bsi; + FrameHeader *fh; + SideInfo *si; + ScaleFactorInfo *sfi; + + /* validate pointers */ + if (!mp3DecInfo || !mp3DecInfo->FrameHeaderPS || !mp3DecInfo->SideInfoPS || !mp3DecInfo->ScaleFactorInfoPS) + return -1; + fh = ((FrameHeader *) (mp3DecInfo->FrameHeaderPS)); + si = ((SideInfo *) (mp3DecInfo->SideInfoPS)); + sfi = ((ScaleFactorInfo *) (mp3DecInfo->ScaleFactorInfoPS)); + + /* init GetBits reader */ + startBuf = buf; + bsi = &bitStreamInfo; + SetBitstreamPointer(bsi, (bitsAvail + *bitOffset + 7) / 8, buf); + if (*bitOffset) + GetBits(bsi, *bitOffset); + + if (fh->ver == MPEG1) + UnpackSFMPEG1(bsi, &si->sis[gr][ch], &sfi->sfis[gr][ch], si->scfsi[ch], gr, &sfi->sfis[0][ch]); + else + UnpackSFMPEG2(bsi, &si->sis[gr][ch], &sfi->sfis[gr][ch], gr, ch, fh->modeExt, &sfi->sfjs); + + mp3DecInfo->part23Length[gr][ch] = si->sis[gr][ch].part23Length; + + bitsUsed = CalcBitsUsed(bsi, buf, *bitOffset); + buf += (bitsUsed + *bitOffset) >> 3; + *bitOffset = (bitsUsed + *bitOffset) & 0x07; + + return (buf - startBuf); +} + +/************************************************************************************** + * Function: AntiAlias + * + * Description: smooth transition across DCT block boundaries (every 18 + *coefficients) + * + * Inputs: vector of dequantized coefficients, length = (nBfly+1) * 18 + * number of "butterflies" to perform (one butterfly means one + * inter-block smoothing operation) + * + * Outputs: updated coefficient vector x + * + * Return: none + * + * Notes: weighted average of opposite bands (pairwise) from the 8 samples + * before and after each block boundary + * nBlocks = (nonZeroBound + 7) / 18, since nZB is the first ZERO + *sample above which all other samples are also zero max gain per sample = 1.372 + * MAX(i) (abs(csa[i][0]) + abs(csa[i][1])) + * bits gained = 0 + * assume at least 1 guard bit in x[] to avoid overflow + * (should be guaranteed from dequant, and max gain from stproc * + *max gain from AntiAlias < 2.0) + **************************************************************************************/ +// a little bit faster in RAM (< 1 ms per block) +static void AntiAlias(int *x, int nBfly) { + int k, a0, b0, c0, c1; + const uint32_t *c; + + /* csa = Q31 */ + for (k = nBfly; k > 0; k--) { + c = csa[0]; + x += 18; + + a0 = x[-1]; + c0 = *c; + c++; + b0 = x[0]; + c1 = *c; + c++; + x[-1] = (MULSHIFT32(c0, a0) - MULSHIFT32(c1, b0)) << 1; + x[0] = (MULSHIFT32(c0, b0) + MULSHIFT32(c1, a0)) << 1; + + a0 = x[-2]; + c0 = *c; + c++; + b0 = x[1]; + c1 = *c; + c++; + x[-2] = (MULSHIFT32(c0, a0) - MULSHIFT32(c1, b0)) << 1; + x[1] = (MULSHIFT32(c0, b0) + MULSHIFT32(c1, a0)) << 1; + + a0 = x[-3]; + c0 = *c; + c++; + b0 = x[2]; + c1 = *c; + c++; + x[-3] = (MULSHIFT32(c0, a0) - MULSHIFT32(c1, b0)) << 1; + x[2] = (MULSHIFT32(c0, b0) + MULSHIFT32(c1, a0)) << 1; + + a0 = x[-4]; + c0 = *c; + c++; + b0 = x[3]; + c1 = *c; + c++; + x[-4] = (MULSHIFT32(c0, a0) - MULSHIFT32(c1, b0)) << 1; + x[3] = (MULSHIFT32(c0, b0) + MULSHIFT32(c1, a0)) << 1; + + a0 = x[-5]; + c0 = *c; + c++; + b0 = x[4]; + c1 = *c; + c++; + x[-5] = (MULSHIFT32(c0, a0) - MULSHIFT32(c1, b0)) << 1; + x[4] = (MULSHIFT32(c0, b0) + MULSHIFT32(c1, a0)) << 1; + + a0 = x[-6]; + c0 = *c; + c++; + b0 = x[5]; + c1 = *c; + c++; + x[-6] = (MULSHIFT32(c0, a0) - MULSHIFT32(c1, b0)) << 1; + x[5] = (MULSHIFT32(c0, b0) + MULSHIFT32(c1, a0)) << 1; + + a0 = x[-7]; + c0 = *c; + c++; + b0 = x[6]; + c1 = *c; + c++; + x[-7] = (MULSHIFT32(c0, a0) - MULSHIFT32(c1, b0)) << 1; + x[6] = (MULSHIFT32(c0, b0) + MULSHIFT32(c1, a0)) << 1; + + a0 = x[-8]; + c0 = *c; + c++; + b0 = x[7]; + c1 = *c; + c++; + x[-8] = (MULSHIFT32(c0, a0) - MULSHIFT32(c1, b0)) << 1; + x[7] = (MULSHIFT32(c0, b0) + MULSHIFT32(c1, a0)) << 1; + } +} + +/************************************************************************************** + * Function: WinPrevious + * + * Description: apply specified window to second half of previous IMDCT (overlap + *part) + * + * Inputs: vector of 9 coefficients (xPrev) + * + * Outputs: 18 windowed output coefficients (gain 1 integer bit) + * window type (0, 1, 2, 3) + * + * Return: none + * + * Notes: produces 9 output samples from 18 input samples via symmetry + * all blocks gain at least 1 guard bit via window (long blocks get + *extra sign bit, short blocks can have one addition but max gain < 1.0) + **************************************************************************************/ +static void WinPrevious(int *xPrev, int *xPrevWin, int btPrev) { + int i, x, *xp, *xpwLo, *xpwHi, wLo, wHi; + const uint32_t *wpLo, *wpHi; + + xp = xPrev; + /* mapping (see IMDCT12x3): xPrev[0-2] = sum[6-8], xPrev[3-8] = sum[12-17] */ + if (btPrev == 2) { + /* this could be reordered for minimum loads/stores */ + wpLo = imdctWin[btPrev]; + xPrevWin[0] = MULSHIFT32(wpLo[6], xPrev[2]) + MULSHIFT32(wpLo[0], xPrev[6]); + xPrevWin[1] = MULSHIFT32(wpLo[7], xPrev[1]) + MULSHIFT32(wpLo[1], xPrev[7]); + xPrevWin[2] = MULSHIFT32(wpLo[8], xPrev[0]) + MULSHIFT32(wpLo[2], xPrev[8]); + xPrevWin[3] = MULSHIFT32(wpLo[9], xPrev[0]) + MULSHIFT32(wpLo[3], xPrev[8]); + xPrevWin[4] = MULSHIFT32(wpLo[10], xPrev[1]) + MULSHIFT32(wpLo[4], xPrev[7]); + xPrevWin[5] = MULSHIFT32(wpLo[11], xPrev[2]) + MULSHIFT32(wpLo[5], xPrev[6]); + xPrevWin[6] = MULSHIFT32(wpLo[6], xPrev[5]); + xPrevWin[7] = MULSHIFT32(wpLo[7], xPrev[4]); + xPrevWin[8] = MULSHIFT32(wpLo[8], xPrev[3]); + xPrevWin[9] = MULSHIFT32(wpLo[9], xPrev[3]); + xPrevWin[10] = MULSHIFT32(wpLo[10], xPrev[4]); + xPrevWin[11] = MULSHIFT32(wpLo[11], xPrev[5]); + xPrevWin[12] = xPrevWin[13] = xPrevWin[14] = xPrevWin[15] = xPrevWin[16] = xPrevWin[17] = 0; + } else { + /* use ARM-style pointers (*ptr++) so that ADS compiles well */ + wpLo = imdctWin[btPrev] + 18; + wpHi = wpLo + 17; + xpwLo = xPrevWin; + xpwHi = xPrevWin + 17; + for (i = 9; i > 0; i--) { + x = *xp++; + wLo = *wpLo++; + wHi = *wpHi--; + *xpwLo++ = MULSHIFT32(wLo, x); + *xpwHi-- = MULSHIFT32(wHi, x); + } + } +} + +/************************************************************************************** + * Function: FreqInvertRescale + * + * Description: do frequency inversion (odd samples of odd blocks) and rescale + * if necessary (extra guard bits added before IMDCT) + * + * Inputs: output vector y (18 new samples, spaced NBANDS apart) + * previous sample vector xPrev (9 samples) + * index of current block + * number of extra shifts added before IMDCT (usually 0) + * + * Outputs: inverted and rescaled (as necessary) outputs + * rescaled (as necessary) previous samples + * + * Return: updated mOut (from new outputs y) + **************************************************************************************/ +static int FreqInvertRescale(int *y, int *xPrev, int blockIdx, int es) { + int i, d, mOut; + int y0, y1, y2, y3, y4, y5, y6, y7, y8; + + if (es == 0) { + /* fast case - frequency invert only (no rescaling) - can fuse into + * overlap-add for speed, if desired */ + if (blockIdx & 0x01) { + y += NBANDS; + y0 = *y; + y += 2 * NBANDS; + y1 = *y; + y += 2 * NBANDS; + y2 = *y; + y += 2 * NBANDS; + y3 = *y; + y += 2 * NBANDS; + y4 = *y; + y += 2 * NBANDS; + y5 = *y; + y += 2 * NBANDS; + y6 = *y; + y += 2 * NBANDS; + y7 = *y; + y += 2 * NBANDS; + y8 = *y; + y += 2 * NBANDS; + + y -= 18 * NBANDS; + *y = -y0; + y += 2 * NBANDS; + *y = -y1; + y += 2 * NBANDS; + *y = -y2; + y += 2 * NBANDS; + *y = -y3; + y += 2 * NBANDS; + *y = -y4; + y += 2 * NBANDS; + *y = -y5; + y += 2 * NBANDS; + *y = -y6; + y += 2 * NBANDS; + *y = -y7; + y += 2 * NBANDS; + *y = -y8; + y += 2 * NBANDS; + } + return 0; + } else { + /* undo pre-IMDCT scaling, clipping if necessary */ + mOut = 0; + if (blockIdx & 0x01) { + /* frequency invert */ + for (i = 0; i < 18; i += 2) { + d = *y; + CLIP_2N(d, 31 - es); + *y = d << es; + mOut |= FASTABS(*y); + y += NBANDS; + d = -*y; + CLIP_2N(d, 31 - es); + *y = d << es; + mOut |= FASTABS(*y); + y += NBANDS; + d = *xPrev; + CLIP_2N(d, 31 - es); + *xPrev++ = d << es; + } + } else { + for (i = 0; i < 18; i += 2) { + d = *y; + CLIP_2N(d, 31 - es); + *y = d << es; + mOut |= FASTABS(*y); + y += NBANDS; + d = *y; + CLIP_2N(d, 31 - es); + *y = d << es; + mOut |= FASTABS(*y); + y += NBANDS; + d = *xPrev; + CLIP_2N(d, 31 - es); + *xPrev++ = d << es; + } + } + return mOut; + } +} + +/* format = Q31 + * #define M_PI 3.14159265358979323846 + * double u = 2.0 * M_PI / 9.0; + * float c0 = sqrt(3.0) / 2.0; + * float c1 = cos(u); + * float c2 = cos(2*u); + * float c3 = sin(u); + * float c4 = sin(2*u); + */ + +static const int c9_0 = 0x6ed9eba1; +static const int c9_1 = 0x620dbe8b; +static const int c9_2 = 0x163a1a7e; +static const int c9_3 = 0x5246dd49; +static const int c9_4 = 0x7e0e2e32; + +/* format = Q31 + * cos(((0:8) + 0.5) * (pi/18)) + */ +static const uint32_t c18[9] = { + 0x7f834ed0, 0x7ba3751d, 0x7401e4c1, 0x68d9f964, 0x5a82799a, 0x496af3e2, 0x36185aee, 0x2120fb83, 0x0b27eb5c, +}; + +/* require at least 3 guard bits in x[] to ensure no overflow */ +static __inline void idct9(int *x) { + int a1, a2, a3, a4, a5, a6, a7, a8, a9; + int a10, a11, a12, a13, a14, a15, a16, a17, a18; + int a19, a20, a21, a22, a23, a24, a25, a26, a27; + int m1, m3, m5, m6, m7, m8, m9, m10, m11, m12; + int x0, x1, x2, x3, x4, x5, x6, x7, x8; + + x0 = x[0]; + x1 = x[1]; + x2 = x[2]; + x3 = x[3]; + x4 = x[4]; + x5 = x[5]; + x6 = x[6]; + x7 = x[7]; + x8 = x[8]; + + a1 = x0 - x6; + a2 = x1 - x5; + a3 = x1 + x5; + a4 = x2 - x4; + a5 = x2 + x4; + a6 = x2 + x8; + a7 = x1 + x7; + + a8 = a6 - a5; /* ie x[8] - x[4] */ + a9 = a3 - a7; /* ie x[5] - x[7] */ + a10 = a2 - x7; /* ie x[1] - x[5] - x[7] */ + a11 = a4 - x8; /* ie x[2] - x[4] - x[8] */ + + /* do the << 1 as constant shifts where mX is actually used (free, no stall or + * extra inst.) */ + m1 = MULSHIFT32(c9_0, x3); + m3 = MULSHIFT32(c9_0, a10); + m5 = MULSHIFT32(c9_1, a5); + m6 = MULSHIFT32(c9_2, a6); + m7 = MULSHIFT32(c9_1, a8); + m8 = MULSHIFT32(c9_2, a5); + m9 = MULSHIFT32(c9_3, a9); + m10 = MULSHIFT32(c9_4, a7); + m11 = MULSHIFT32(c9_3, a3); + m12 = MULSHIFT32(c9_4, a9); + + a12 = x[0] + (x[6] >> 1); + a13 = a12 + (m1 << 1); + a14 = a12 - (m1 << 1); + a15 = a1 + (a11 >> 1); + a16 = (m5 << 1) + (m6 << 1); + a17 = (m7 << 1) - (m8 << 1); + a18 = a16 + a17; + a19 = (m9 << 1) + (m10 << 1); + a20 = (m11 << 1) - (m12 << 1); + + a21 = a20 - a19; + a22 = a13 + a16; + a23 = a14 + a16; + a24 = a14 + a17; + a25 = a13 + a17; + a26 = a14 - a18; + a27 = a13 - a18; + + x0 = a22 + a19; + x[0] = x0; + x1 = a15 + (m3 << 1); + x[1] = x1; + x2 = a24 + a20; + x[2] = x2; + x3 = a26 - a21; + x[3] = x3; + x4 = a1 - a11; + x[4] = x4; + x5 = a27 + a21; + x[5] = x5; + x6 = a25 - a20; + x[6] = x6; + x7 = a15 - (m3 << 1); + x[7] = x7; + x8 = a23 - a19; + x[8] = x8; +} + +/* let c(j) = cos(M_PI/36 * ((j)+0.5)), s(j) = sin(M_PI/36 * ((j)+0.5)) + * then fastWin[2*j+0] = c(j)*(s(j) + c(j)), j = [0, 8] + * fastWin[2*j+1] = c(j)*(s(j) - c(j)) + * format = Q30 + */ +const uint32_t fastWin36[18] = { + 0x42aace8b, 0xc2e92724, 0x47311c28, 0xc95f619a, 0x4a868feb, 0xd0859d8c, 0x4c913b51, 0xd8243ea0, 0x4d413ccc, + 0xe0000000, 0x4c913b51, 0xe7dbc161, 0x4a868feb, 0xef7a6275, 0x47311c28, 0xf6a09e67, 0x42aace8b, 0xfd16d8dd, +}; + +/************************************************************************************** + * Function: IMDCT36 + * + * Description: 36-point modified DCT, with windowing and overlap-add (50% + *overlap) + * + * Inputs: vector of 18 coefficients (N/2 inputs produces N outputs, by + *symmetry) overlap part of last IMDCT (9 samples - see output comments) window + *type (0,1,2,3) of current and previous block current block index (for deciding + *whether to do frequency inversion) number of guard bits in input vector + * + * Outputs: 18 output samples, after windowing and overlap-add with last + *frame second half of (unwindowed) 36-point IMDCT - save for next time only + *save 9 xPrev samples, using symmetry (see WinPrevious()) + * + * Notes: this is Ken's hyper-fast algorithm, including symmetric sin + *window optimization, if applicable total number of multiplies, general case: + * 2*10 (idct9) + 9 (last stage imdct) + 36 (for windowing) = 65 + * total number of multiplies, btCurr == 0 && btPrev == 0: + * 2*10 (idct9) + 9 (last stage imdct) + 18 (for windowing) = 47 + * + * blockType == 0 is by far the most common case, so it should be + * possible to use the fast path most of the time + * this is the fastest known algorithm for performing + * long IMDCT + windowing + overlap-add in MP3 + * + * Return: mOut (OR of abs(y) for all y calculated here) + * + * TODO: optimize for ARM (reorder window coefs, ARM-style pointers in C, + * inline asm may or may not be helpful) + **************************************************************************************/ +// barely faster in RAM +static int IMDCT36(int *xCurr, int *xPrev, int *y, int btCurr, int btPrev, int blockIdx, int gb) { + int i, es, xBuf[18], xPrevWin[18]; + int acc1, acc2, s, d, t, mOut; + int xo, xe, c, *xp, yLo, yHi; + const uint32_t *cp, *wp; + + acc1 = acc2 = 0; + xCurr += 17; + + /* 7 gb is always adequate for antialias + accumulator loop + idct9 */ + if (gb < 7) { + /* rarely triggered - 5% to 10% of the time on normal clips (with Q25 input) + */ + es = 7 - gb; + for (i = 8; i >= 0; i--) { + acc1 = ((*xCurr--) >> es) - acc1; + acc2 = acc1 - acc2; + acc1 = ((*xCurr--) >> es) - acc1; + xBuf[i + 9] = acc2; /* odd */ + xBuf[i + 0] = acc1; /* even */ + xPrev[i] >>= es; + } + } else { + es = 0; + /* max gain = 18, assume adequate guard bits */ + for (i = 8; i >= 0; i--) { + acc1 = (*xCurr--) - acc1; + acc2 = acc1 - acc2; + acc1 = (*xCurr--) - acc1; + xBuf[i + 9] = acc2; /* odd */ + xBuf[i + 0] = acc1; /* even */ + } + } + /* xEven[0] and xOdd[0] scaled by 0.5 */ + xBuf[9] >>= 1; + xBuf[0] >>= 1; + + /* do 9-point IDCT on even and odd */ + idct9(xBuf + 0); /* even */ + idct9(xBuf + 9); /* odd */ + + xp = xBuf + 8; + cp = c18 + 8; + mOut = 0; + if (btPrev == 0 && btCurr == 0) { + /* fast path - use symmetry of sin window to reduce windowing multiplies to + * 18 (N/2) */ + wp = fastWin36; + for (i = 0; i < 9; i++) { + /* do ARM-style pointer arithmetic (i still needed for y[] indexing - + * compiler spills if 2 y pointers) */ + c = *cp--; + xo = *(xp + 9); + xe = *xp--; + /* gain 2 int bits here */ + xo = MULSHIFT32(c, xo); /* 2*c18*xOdd (mul by 2 implicit in scaling) */ + xe >>= 2; + + s = -(*xPrev); /* sum from last block (always at least 2 guard bits) */ + d = -(xe - xo); /* gain 2 int bits, don't shift xo (effective << 1 to eat + sign bit, << 1 for mul by 2) */ + (*xPrev++) = xe + xo; /* symmetry - xPrev[i] = xPrev[17-i] for long blocks */ + t = s - d; + + yLo = (d + (MULSHIFT32(t, *wp++) << 2)); + yHi = (s + (MULSHIFT32(t, *wp++) << 2)); + y[(i) *NBANDS] = yLo; + y[(17 - i) * NBANDS] = yHi; + mOut |= FASTABS(yLo); + mOut |= FASTABS(yHi); + } + } else { + /* slower method - either prev or curr is using window type != 0 so do full + * 36-point window output xPrevWin has at least 3 guard bits (xPrev has 2, + * gain 1 in WinPrevious) + */ + WinPrevious(xPrev, xPrevWin, btPrev); + + wp = imdctWin[btCurr]; + for (i = 0; i < 9; i++) { + c = *cp--; + xo = *(xp + 9); + xe = *xp--; + /* gain 2 int bits here */ + xo = MULSHIFT32(c, xo); /* 2*c18*xOdd (mul by 2 implicit in scaling) */ + xe >>= 2; + + d = xe - xo; + (*xPrev++) = xe + xo; /* symmetry - xPrev[i] = xPrev[17-i] for long blocks */ + + yLo = (xPrevWin[i] + MULSHIFT32(d, wp[i])) << 2; + yHi = (xPrevWin[17 - i] + MULSHIFT32(d, wp[17 - i])) << 2; + y[(i) *NBANDS] = yLo; + y[(17 - i) * NBANDS] = yHi; + mOut |= FASTABS(yLo); + mOut |= FASTABS(yHi); + } + } + + xPrev -= 9; + mOut |= FreqInvertRescale(y, xPrev, blockIdx, es); + + return mOut; +} + +static int c3_0 = 0x6ed9eba1; /* format = Q31, cos(pi/6) */ +static int c6[3] = {0x7ba3751d, 0x5a82799a, 0x2120fb83}; /* format = Q31, cos(((0:2) + 0.5) * (pi/6)) */ + +/* 12-point inverse DCT, used in IMDCT12x3() + * 4 input guard bits will ensure no overflow + */ +static __inline void imdct12(int *x, int *out) { + int a0, a1, a2; + int x0, x1, x2, x3, x4, x5; + + x0 = *x; + x += 3; + x1 = *x; + x += 3; + x2 = *x; + x += 3; + x3 = *x; + x += 3; + x4 = *x; + x += 3; + x5 = *x; + x += 3; + + x4 -= x5; + x3 -= x4; + x2 -= x3; + x3 -= x5; + x1 -= x2; + x0 -= x1; + x1 -= x3; + + x0 >>= 1; + x1 >>= 1; + + a0 = MULSHIFT32(c3_0, x2) << 1; + a1 = x0 + (x4 >> 1); + a2 = x0 - x4; + x0 = a1 + a0; + x2 = a2; + x4 = a1 - a0; + + a0 = MULSHIFT32(c3_0, x3) << 1; + a1 = x1 + (x5 >> 1); + a2 = x1 - x5; + + /* cos window odd samples, mul by 2, eat sign bit */ + x1 = MULSHIFT32(c6[0], a1 + a0) << 2; + x3 = MULSHIFT32(c6[1], a2) << 2; + x5 = MULSHIFT32(c6[2], a1 - a0) << 2; + + *out = x0 + x1; + out++; + *out = x2 + x3; + out++; + *out = x4 + x5; + out++; + *out = x4 - x5; + out++; + *out = x2 - x3; + out++; + *out = x0 - x1; +} + +/************************************************************************************** + * Function: IMDCT12x3 + * + * Description: three 12-point modified DCT's for short blocks, with windowing, + * short block concatenation, and overlap-add + * + * Inputs: 3 interleaved vectors of 6 samples each + * (block0[0], block1[0], block2[0], block0[1], block1[1]....) + * overlap part of last IMDCT (9 samples - see output comments) + * window type (0,1,2,3) of previous block + * current block index (for deciding whether to do frequency + *inversion) number of guard bits in input vector + * + * Outputs: updated sample vector x, net gain of 1 integer bit + * second half of (unwindowed) IMDCT's - save for next time + * only save 9 xPrev samples, using symmetry (see WinPrevious()) + * + * Return: mOut (OR of abs(y) for all y calculated here) + * + * TODO: optimize for ARM + **************************************************************************************/ +// barely faster in RAM +static int IMDCT12x3(int *xCurr, int *xPrev, int *y, int btPrev, int blockIdx, int gb) { + int i, es, mOut, yLo, xBuf[18], xPrevWin[18]; /* need temp buffer for reordering short blocks */ + const uint32_t *wp; + + es = 0; + /* 7 gb is always adequate for accumulator loop + idct12 + window + overlap */ + if (gb < 7) { + es = 7 - gb; + for (i = 0; i < 18; i += 2) { + xCurr[i + 0] >>= es; + xCurr[i + 1] >>= es; + *xPrev++ >>= es; + } + xPrev -= 9; + } + + /* requires 4 input guard bits for each imdct12 */ + imdct12(xCurr + 0, xBuf + 0); + imdct12(xCurr + 1, xBuf + 6); + imdct12(xCurr + 2, xBuf + 12); + + /* window previous from last time */ + WinPrevious(xPrev, xPrevWin, btPrev); + + /* could unroll this for speed, minimum loads (short blocks usually rare, so + * doesn't make much overall difference) xPrevWin[i] << 2 still has 1 gb + * always, max gain of windowed xBuf stuff also < 1.0 and gain the sign bit so + * y calculations won't overflow + */ + wp = imdctWin[2]; + mOut = 0; + for (i = 0; i < 3; i++) { + yLo = (xPrevWin[0 + i] << 2); + mOut |= FASTABS(yLo); + y[(0 + i) * NBANDS] = yLo; + yLo = (xPrevWin[3 + i] << 2); + mOut |= FASTABS(yLo); + y[(3 + i) * NBANDS] = yLo; + yLo = (xPrevWin[6 + i] << 2) + (MULSHIFT32(wp[0 + i], xBuf[3 + i])); + mOut |= FASTABS(yLo); + y[(6 + i) * NBANDS] = yLo; + yLo = (xPrevWin[9 + i] << 2) + (MULSHIFT32(wp[3 + i], xBuf[5 - i])); + mOut |= FASTABS(yLo); + y[(9 + i) * NBANDS] = yLo; + yLo = (xPrevWin[12 + i] << 2) + (MULSHIFT32(wp[6 + i], xBuf[2 - i]) + MULSHIFT32(wp[0 + i], xBuf[(6 + 3) + i])); + mOut |= FASTABS(yLo); + y[(12 + i) * NBANDS] = yLo; + yLo = (xPrevWin[15 + i] << 2) + (MULSHIFT32(wp[9 + i], xBuf[0 + i]) + MULSHIFT32(wp[3 + i], xBuf[(6 + 5) - i])); + mOut |= FASTABS(yLo); + y[(15 + i) * NBANDS] = yLo; + } + + /* save previous (unwindowed) for overlap - only need samples 6-8, 12-17 */ + for (i = 6; i < 9; i++) + *xPrev++ = xBuf[i] >> 2; + for (i = 12; i < 18; i++) + *xPrev++ = xBuf[i] >> 2; + + xPrev -= 9; + mOut |= FreqInvertRescale(y, xPrev, blockIdx, es); + + return mOut; +} + +/************************************************************************************** + * Function: HybridTransform + * + * Description: IMDCT's, windowing, and overlap-add on long/short/mixed blocks + * + * Inputs: vector of input coefficients, length = nBlocksTotal * 18) + * vector of overlap samples from last time, length = nBlocksPrev * + *9) buffer for output samples, length = MAXNSAMP SideInfoSub struct for this + *granule/channel BlockCount struct with necessary info number of non-zero input + *and overlap blocks number of long blocks in input vector (rest assumed to be + *short blocks) number of blocks which use long window (type) 0 in case of mixed + *block (bc->currWinSwitch, 0 for non-mixed blocks) + * + * Outputs: transformed, windowed, and overlapped sample buffer + * does frequency inversion on odd blocks + * updated buffer of samples for overlap + * + * Return: number of non-zero IMDCT blocks calculated in this call + * (including overlap-add) + * + * TODO: examine mixedBlock/winSwitch logic carefully (test he_mode.bit) + **************************************************************************************/ +static int HybridTransform(int *xCurr, int *xPrev, int y[BLOCK_SIZE][NBANDS], SideInfoSub *sis, BlockCount *bc) { + int xPrevWin[18], currWinIdx, prevWinIdx; + int i, j, nBlocksOut, nonZero, mOut; + int fiBit, xp; + + ASSERT(bc->nBlocksLong <= NBANDS); + ASSERT(bc->nBlocksTotal <= NBANDS); + ASSERT(bc->nBlocksPrev <= NBANDS); + + mOut = 0; + + /* do long blocks, if any */ + for (i = 0; i < bc->nBlocksLong; i++) { + /* currWinIdx picks the right window for long blocks (if mixed, long blocks + * use window type 0) */ + currWinIdx = sis->blockType; + if (sis->mixedBlock && i < bc->currWinSwitch) + currWinIdx = 0; + + prevWinIdx = bc->prevType; + if (i < bc->prevWinSwitch) + prevWinIdx = 0; + + /* do 36-point IMDCT, including windowing and overlap-add */ + mOut |= IMDCT36(xCurr, xPrev, &(y[0][i]), currWinIdx, prevWinIdx, i, bc->gbIn); + xCurr += 18; + xPrev += 9; + } + + /* do short blocks (if any) */ + for (; i < bc->nBlocksTotal; i++) { + ASSERT(sis->blockType == 2); + + prevWinIdx = bc->prevType; + if (i < bc->prevWinSwitch) + prevWinIdx = 0; + + mOut |= IMDCT12x3(xCurr, xPrev, &(y[0][i]), prevWinIdx, i, bc->gbIn); + xCurr += 18; + xPrev += 9; + } + nBlocksOut = i; + + /* window and overlap prev if prev longer that current */ + for (; i < bc->nBlocksPrev; i++) { + prevWinIdx = bc->prevType; + if (i < bc->prevWinSwitch) + prevWinIdx = 0; + WinPrevious(xPrev, xPrevWin, prevWinIdx); + + nonZero = 0; + fiBit = i << 31; + for (j = 0; j < 9; j++) { + xp = xPrevWin[2 * j + 0] << 2; /* << 2 temp for scaling */ + nonZero |= xp; + y[2 * j + 0][i] = xp; + mOut |= FASTABS(xp); + + /* frequency inversion on odd blocks/odd samples (flip sign if i odd, j + * odd) */ + xp = xPrevWin[2 * j + 1] << 2; + xp = (xp ^ (fiBit >> 31)) + (i & 0x01); + nonZero |= xp; + y[2 * j + 1][i] = xp; + mOut |= FASTABS(xp); + + xPrev[j] = 0; + } + xPrev += 9; + if (nonZero) + nBlocksOut = i; + } + + /* clear rest of blocks */ + for (; i < 32; i++) { + for (j = 0; j < 18; j++) + y[j][i] = 0; + } + + bc->gbOut = CLZ(mOut) - 1; + + return nBlocksOut; +} + +/************************************************************************************** + * Function: IMDCT + * + * Description: do alias reduction, inverse MDCT, overlap-add, and frequency + *inversion + * + * Inputs: MP3DecInfo structure filled by UnpackFrameHeader(), + *UnpackSideInfo(), UnpackScaleFactors(), and DecodeHuffman() (for this granule, + *channel) includes PCM samples in overBuf (from last call to IMDCT) for OLA + * index of current granule and channel + * + * Outputs: PCM samples in outBuf, for input to subband transform + * PCM samples in overBuf, for OLA next time + * updated hi->nonZeroBound index for this channel + * + * Return: 0 on success, -1 if null input pointers + **************************************************************************************/ +// a bit faster in RAM +int IMDCT(MP3DecInfo *mp3DecInfo, int gr, int ch) { + int nBfly, blockCutoff; + FrameHeader *fh; + SideInfo *si; + HuffmanInfo *hi; + IMDCTInfo *mi; + BlockCount bc; + + /* validate pointers */ + if (!mp3DecInfo || !mp3DecInfo->FrameHeaderPS || !mp3DecInfo->SideInfoPS || !mp3DecInfo->HuffmanInfoPS || + !mp3DecInfo->IMDCTInfoPS) + return -1; + + /* si is an array of up to 4 structs, stored as gr0ch0, gr0ch1, gr1ch0, gr1ch1 + */ + fh = (FrameHeader *) (mp3DecInfo->FrameHeaderPS); + si = (SideInfo *) (mp3DecInfo->SideInfoPS); + hi = (HuffmanInfo *) (mp3DecInfo->HuffmanInfoPS); + mi = (IMDCTInfo *) (mp3DecInfo->IMDCTInfoPS); + + /* anti-aliasing done on whole long blocks only + * for mixed blocks, nBfly always 1, except 3 for 8 kHz MPEG 2.5 (see + * sfBandTab) nLongBlocks = number of blocks with (possibly) non-zero power + * nBfly = number of butterflies to do (nLongBlocks - 1, unless no long + * blocks) + */ + blockCutoff = fh->sfBand->l[(fh->ver == MPEG1 ? 8 : 6)] / 18; /* same as 3* num short sfb's in spec */ + if (si->sis[gr][ch].blockType != 2) { + /* all long transforms */ + bc.nBlocksLong = MIN((hi->nonZeroBound[ch] + 7) / 18 + 1, 32); + nBfly = bc.nBlocksLong - 1; + } else if (si->sis[gr][ch].blockType == 2 && si->sis[gr][ch].mixedBlock) { + /* mixed block - long transforms until cutoff, then short transforms */ + bc.nBlocksLong = blockCutoff; + nBfly = bc.nBlocksLong - 1; + } else { + /* all short transforms */ + bc.nBlocksLong = 0; + nBfly = 0; + } + + AntiAlias(hi->huffDecBuf[ch], nBfly); + hi->nonZeroBound[ch] = MAX(hi->nonZeroBound[ch], (nBfly * 18) + 8); + + ASSERT(hi->nonZeroBound[ch] <= MAX_NSAMP); + + /* for readability, use a struct instead of passing a million parameters to + * HybridTransform() */ + bc.nBlocksTotal = (hi->nonZeroBound[ch] + 17) / 18; + bc.nBlocksPrev = mi->numPrevIMDCT[ch]; + bc.prevType = mi->prevType[ch]; + bc.prevWinSwitch = mi->prevWinSwitch[ch]; + bc.currWinSwitch = (si->sis[gr][ch].mixedBlock ? blockCutoff : 0); /* where WINDOW switches (not nec. transform) */ + bc.gbIn = hi->gb[ch]; + + mi->numPrevIMDCT[ch] = HybridTransform(hi->huffDecBuf[ch], mi->overBuf[ch], mi->outBuf[ch], &si->sis[gr][ch], &bc); + mi->prevType[ch] = si->sis[gr][ch].blockType; + mi->prevWinSwitch[ch] = bc.currWinSwitch; /* 0 means not a mixed block (either + all short or all long) */ + mi->gb[ch] = bc.gbOut; + + ASSERT(mi->numPrevIMDCT[ch] <= NBANDS); + + /* output has gained 2 int bits */ + return 0; +} + +/* NOTE - regenerated tables to use shorts instead of ints + * (all needed data can fit in 16 bits - see below) + * + * format 0xABCD + * A = length of codeword + * B = y value + * C = x value + * D = number of sign bits (0, 1, or 2) + * + * to read a CW, the code reads maxbits from the stream (dep. on + * table index), but doesn't remove them from the bitstream reader + * then it gets the correct CW by direct lookup into the table + * of length (2^maxbits) (more complicated for non-oneShot...) + * for CW's with hlen < maxbits, there are multiple entries in the + * table (extra bits are don't cares) + * the bitstream reader then "purges" (or removes) only the correct + * number of bits for the chosen CW + * + * entries starting with F are special: D (signbits) is maxbits, + * so the decoder always checks huffTableXX[0] first, gets the + * signbits, and reads that many bits from the bitstream + * (sometimes it takes > 1 read to get the value, so maxbits is + * can get updated by jumping to another value starting with 0xF) + * entries starting with 0 are also special: A = hlen = 0, rest of + * value is an offset to jump higher in the table (for tables of + * type loopNoLinbits or loopLinbits) + */ + +/* store Huffman codes as one big table plus table of offsets, since some + * platforms don't properly support table-of-tables (table of pointers to other + * const tables) + */ +const unsigned short huffTable[] = { + /* huffTable01[9] */ + 0xf003, + 0x3112, + 0x3101, + 0x2011, + 0x2011, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + + /* huffTable02[65] */ + 0xf006, + 0x6222, + 0x6201, + 0x5212, + 0x5212, + 0x5122, + 0x5122, + 0x5021, + 0x5021, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + + /* huffTable03[65] */ + 0xf006, + 0x6222, + 0x6201, + 0x5212, + 0x5212, + 0x5122, + 0x5122, + 0x5021, + 0x5021, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2101, + 0x2101, + 0x2101, + 0x2101, + 0x2101, + 0x2101, + 0x2101, + 0x2101, + 0x2101, + 0x2101, + 0x2101, + 0x2101, + 0x2101, + 0x2101, + 0x2101, + 0x2101, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + + /* huffTable05[257] */ + 0xf008, + 0x8332, + 0x8322, + 0x7232, + 0x7232, + 0x6132, + 0x6132, + 0x6132, + 0x6132, + 0x7312, + 0x7312, + 0x7301, + 0x7301, + 0x7031, + 0x7031, + 0x7222, + 0x7222, + 0x6212, + 0x6212, + 0x6212, + 0x6212, + 0x6122, + 0x6122, + 0x6122, + 0x6122, + 0x6201, + 0x6201, + 0x6201, + 0x6201, + 0x6021, + 0x6021, + 0x6021, + 0x6021, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + + /* huffTable06[129] */ + 0xf007, + 0x7332, + 0x7301, + 0x6322, + 0x6322, + 0x6232, + 0x6232, + 0x6031, + 0x6031, + 0x5312, + 0x5312, + 0x5312, + 0x5312, + 0x5132, + 0x5132, + 0x5132, + 0x5132, + 0x5222, + 0x5222, + 0x5222, + 0x5222, + 0x5201, + 0x5201, + 0x5201, + 0x5201, + 0x4212, + 0x4212, + 0x4212, + 0x4212, + 0x4212, + 0x4212, + 0x4212, + 0x4212, + 0x4122, + 0x4122, + 0x4122, + 0x4122, + 0x4122, + 0x4122, + 0x4122, + 0x4122, + 0x4021, + 0x4021, + 0x4021, + 0x4021, + 0x4021, + 0x4021, + 0x4021, + 0x4021, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3000, + 0x3000, + 0x3000, + 0x3000, + 0x3000, + 0x3000, + 0x3000, + 0x3000, + 0x3000, + 0x3000, + 0x3000, + 0x3000, + 0x3000, + 0x3000, + 0x3000, + 0x3000, + + /* huffTable07[110] */ + 0xf006, + 0x0041, + 0x0052, + 0x005b, + 0x0060, + 0x0063, + 0x0068, + 0x006b, + 0x6212, + 0x5122, + 0x5122, + 0x6201, + 0x6021, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0xf004, + 0x4552, + 0x4542, + 0x4452, + 0x4352, + 0x3532, + 0x3532, + 0x3442, + 0x3442, + 0x3522, + 0x3522, + 0x3252, + 0x3252, + 0x2512, + 0x2512, + 0x2512, + 0x2512, + 0xf003, + 0x2152, + 0x2152, + 0x3501, + 0x3432, + 0x2051, + 0x2051, + 0x3342, + 0x3332, + 0xf002, + 0x2422, + 0x2242, + 0x1412, + 0x1412, + 0xf001, + 0x1142, + 0x1041, + 0xf002, + 0x2401, + 0x2322, + 0x2232, + 0x2301, + 0xf001, + 0x1312, + 0x1132, + 0xf001, + 0x1031, + 0x1222, + + /* huffTable08[280] */ + 0xf008, + 0x0101, + 0x010a, + 0x010f, + 0x8512, + 0x8152, + 0x0112, + 0x0115, + 0x8422, + 0x8242, + 0x8412, + 0x7142, + 0x7142, + 0x8401, + 0x8041, + 0x8322, + 0x8232, + 0x8312, + 0x8132, + 0x8301, + 0x8031, + 0x6222, + 0x6222, + 0x6222, + 0x6222, + 0x6201, + 0x6201, + 0x6201, + 0x6201, + 0x6021, + 0x6021, + 0x6021, + 0x6021, + 0x4212, + 0x4212, + 0x4212, + 0x4212, + 0x4212, + 0x4212, + 0x4212, + 0x4212, + 0x4212, + 0x4212, + 0x4212, + 0x4212, + 0x4212, + 0x4212, + 0x4212, + 0x4212, + 0x4122, + 0x4122, + 0x4122, + 0x4122, + 0x4122, + 0x4122, + 0x4122, + 0x4122, + 0x4122, + 0x4122, + 0x4122, + 0x4122, + 0x4122, + 0x4122, + 0x4122, + 0x4122, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x2112, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0xf003, + 0x3552, + 0x3452, + 0x2542, + 0x2542, + 0x1352, + 0x1352, + 0x1352, + 0x1352, + 0xf002, + 0x2532, + 0x2442, + 0x1522, + 0x1522, + 0xf001, + 0x1252, + 0x1501, + 0xf001, + 0x1432, + 0x1342, + 0xf001, + 0x1051, + 0x1332, + + /* huffTable09[93] */ + 0xf006, + 0x0041, + 0x004a, + 0x004f, + 0x0052, + 0x0057, + 0x005a, + 0x6412, + 0x6142, + 0x6322, + 0x6232, + 0x5312, + 0x5312, + 0x5132, + 0x5132, + 0x6301, + 0x6031, + 0x5222, + 0x5222, + 0x5201, + 0x5201, + 0x4212, + 0x4212, + 0x4212, + 0x4212, + 0x4122, + 0x4122, + 0x4122, + 0x4122, + 0x4021, + 0x4021, + 0x4021, + 0x4021, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3000, + 0x3000, + 0x3000, + 0x3000, + 0x3000, + 0x3000, + 0x3000, + 0x3000, + 0xf003, + 0x3552, + 0x3542, + 0x2532, + 0x2532, + 0x2352, + 0x2352, + 0x3452, + 0x3501, + 0xf002, + 0x2442, + 0x2522, + 0x2252, + 0x2512, + 0xf001, + 0x1152, + 0x1432, + 0xf002, + 0x1342, + 0x1342, + 0x2051, + 0x2401, + 0xf001, + 0x1422, + 0x1242, + 0xf001, + 0x1332, + 0x1041, + + /* huffTable10[320] */ + 0xf008, + 0x0101, + 0x010a, + 0x010f, + 0x0118, + 0x011b, + 0x0120, + 0x0125, + 0x8712, + 0x8172, + 0x012a, + 0x012d, + 0x0132, + 0x8612, + 0x8162, + 0x8061, + 0x0137, + 0x013a, + 0x013d, + 0x8412, + 0x8142, + 0x8041, + 0x8322, + 0x8232, + 0x8301, + 0x7312, + 0x7312, + 0x7132, + 0x7132, + 0x7031, + 0x7031, + 0x7222, + 0x7222, + 0x6212, + 0x6212, + 0x6212, + 0x6212, + 0x6122, + 0x6122, + 0x6122, + 0x6122, + 0x6201, + 0x6201, + 0x6201, + 0x6201, + 0x6021, + 0x6021, + 0x6021, + 0x6021, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0xf003, + 0x3772, + 0x3762, + 0x3672, + 0x3752, + 0x3572, + 0x3662, + 0x2742, + 0x2742, + 0xf002, + 0x2472, + 0x2652, + 0x2562, + 0x2732, + 0xf003, + 0x2372, + 0x2372, + 0x2642, + 0x2642, + 0x3552, + 0x3452, + 0x2362, + 0x2362, + 0xf001, + 0x1722, + 0x1272, + 0xf002, + 0x2462, + 0x2701, + 0x1071, + 0x1071, + 0xf002, + 0x1262, + 0x1262, + 0x2542, + 0x2532, + 0xf002, + 0x1601, + 0x1601, + 0x2352, + 0x2442, + 0xf001, + 0x1632, + 0x1622, + 0xf002, + 0x2522, + 0x2252, + 0x1512, + 0x1512, + 0xf002, + 0x1152, + 0x1152, + 0x2432, + 0x2342, + 0xf001, + 0x1501, + 0x1051, + 0xf001, + 0x1422, + 0x1242, + 0xf001, + 0x1332, + 0x1401, + + /* huffTable11[296] */ + 0xf008, + 0x0101, + 0x0106, + 0x010f, + 0x0114, + 0x0117, + 0x8722, + 0x8272, + 0x011c, + 0x7172, + 0x7172, + 0x8712, + 0x8071, + 0x8632, + 0x8362, + 0x8061, + 0x011f, + 0x0122, + 0x8512, + 0x7262, + 0x7262, + 0x8622, + 0x8601, + 0x7612, + 0x7612, + 0x7162, + 0x7162, + 0x8152, + 0x8432, + 0x8051, + 0x0125, + 0x8422, + 0x8242, + 0x8412, + 0x8142, + 0x8401, + 0x8041, + 0x7322, + 0x7322, + 0x7232, + 0x7232, + 0x6312, + 0x6312, + 0x6312, + 0x6312, + 0x6132, + 0x6132, + 0x6132, + 0x6132, + 0x7301, + 0x7301, + 0x7031, + 0x7031, + 0x6222, + 0x6222, + 0x6222, + 0x6222, + 0x5122, + 0x5122, + 0x5122, + 0x5122, + 0x5122, + 0x5122, + 0x5122, + 0x5122, + 0x4212, + 0x4212, + 0x4212, + 0x4212, + 0x4212, + 0x4212, + 0x4212, + 0x4212, + 0x4212, + 0x4212, + 0x4212, + 0x4212, + 0x4212, + 0x4212, + 0x4212, + 0x4212, + 0x5201, + 0x5201, + 0x5201, + 0x5201, + 0x5201, + 0x5201, + 0x5201, + 0x5201, + 0x5021, + 0x5021, + 0x5021, + 0x5021, + 0x5021, + 0x5021, + 0x5021, + 0x5021, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0x2000, + 0xf002, + 0x2772, + 0x2762, + 0x2672, + 0x2572, + 0xf003, + 0x2662, + 0x2662, + 0x2742, + 0x2742, + 0x2472, + 0x2472, + 0x3752, + 0x3552, + 0xf002, + 0x2652, + 0x2562, + 0x1732, + 0x1732, + 0xf001, + 0x1372, + 0x1642, + 0xf002, + 0x2542, + 0x2452, + 0x2532, + 0x2352, + 0xf001, + 0x1462, + 0x1701, + 0xf001, + 0x1442, + 0x1522, + 0xf001, + 0x1252, + 0x1501, + 0xf001, + 0x1342, + 0x1332, + + /* huffTable12[185] */ + 0xf007, + 0x0081, + 0x008a, + 0x008f, + 0x0092, + 0x0097, + 0x009a, + 0x009d, + 0x00a2, + 0x00a5, + 0x00a8, + 0x7622, + 0x7262, + 0x7162, + 0x00ad, + 0x00b0, + 0x00b3, + 0x7512, + 0x7152, + 0x7432, + 0x7342, + 0x00b6, + 0x7422, + 0x7242, + 0x7412, + 0x6332, + 0x6332, + 0x6142, + 0x6142, + 0x6322, + 0x6322, + 0x6232, + 0x6232, + 0x7041, + 0x7301, + 0x6031, + 0x6031, + 0x5312, + 0x5312, + 0x5312, + 0x5312, + 0x5132, + 0x5132, + 0x5132, + 0x5132, + 0x5222, + 0x5222, + 0x5222, + 0x5222, + 0x4212, + 0x4212, + 0x4212, + 0x4212, + 0x4212, + 0x4212, + 0x4212, + 0x4212, + 0x4122, + 0x4122, + 0x4122, + 0x4122, + 0x4122, + 0x4122, + 0x4122, + 0x4122, + 0x5201, + 0x5201, + 0x5201, + 0x5201, + 0x5021, + 0x5021, + 0x5021, + 0x5021, + 0x4000, + 0x4000, + 0x4000, + 0x4000, + 0x4000, + 0x4000, + 0x4000, + 0x4000, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3101, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0xf003, + 0x3772, + 0x3762, + 0x2672, + 0x2672, + 0x2752, + 0x2752, + 0x2572, + 0x2572, + 0xf002, + 0x2662, + 0x2742, + 0x2472, + 0x2562, + 0xf001, + 0x1652, + 0x1732, + 0xf002, + 0x2372, + 0x2552, + 0x1722, + 0x1722, + 0xf001, + 0x1272, + 0x1642, + 0xf001, + 0x1462, + 0x1712, + 0xf002, + 0x1172, + 0x1172, + 0x2701, + 0x2071, + 0xf001, + 0x1632, + 0x1362, + 0xf001, + 0x1542, + 0x1452, + 0xf002, + 0x1442, + 0x1442, + 0x2601, + 0x2501, + 0xf001, + 0x1612, + 0x1061, + 0xf001, + 0x1532, + 0x1352, + 0xf001, + 0x1522, + 0x1252, + 0xf001, + 0x1051, + 0x1401, + + /* huffTable13[497] */ + 0xf006, + 0x0041, + 0x0082, + 0x00c3, + 0x00e4, + 0x0105, + 0x0116, + 0x011f, + 0x0130, + 0x0139, + 0x013e, + 0x0143, + 0x0146, + 0x6212, + 0x6122, + 0x6201, + 0x6021, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0xf006, + 0x0108, + 0x0111, + 0x011a, + 0x0123, + 0x012c, + 0x0131, + 0x0136, + 0x013f, + 0x0144, + 0x0147, + 0x014c, + 0x0151, + 0x0156, + 0x015b, + 0x6f12, + 0x61f2, + 0x60f1, + 0x0160, + 0x0163, + 0x0166, + 0x62e2, + 0x0169, + 0x6e12, + 0x61e2, + 0x016c, + 0x016f, + 0x0172, + 0x0175, + 0x0178, + 0x017b, + 0x66c2, + 0x6d32, + 0x017e, + 0x6d22, + 0x62d2, + 0x6d12, + 0x67b2, + 0x0181, + 0x0184, + 0x63c2, + 0x0187, + 0x6b42, + 0x51d2, + 0x51d2, + 0x6d01, + 0x60d1, + 0x6a82, + 0x68a2, + 0x6c42, + 0x64c2, + 0x6b62, + 0x66b2, + 0x5c32, + 0x5c32, + 0x5c22, + 0x5c22, + 0x52c2, + 0x52c2, + 0x5b52, + 0x5b52, + 0x65b2, + 0x6982, + 0x5c12, + 0x5c12, + 0xf006, + 0x51c2, + 0x51c2, + 0x6892, + 0x6c01, + 0x50c1, + 0x50c1, + 0x64b2, + 0x6a62, + 0x66a2, + 0x6972, + 0x5b32, + 0x5b32, + 0x53b2, + 0x53b2, + 0x6882, + 0x6a52, + 0x5b22, + 0x5b22, + 0x65a2, + 0x6962, + 0x54a2, + 0x54a2, + 0x6872, + 0x6782, + 0x5492, + 0x5492, + 0x6772, + 0x6672, + 0x42b2, + 0x42b2, + 0x42b2, + 0x42b2, + 0x4b12, + 0x4b12, + 0x4b12, + 0x4b12, + 0x41b2, + 0x41b2, + 0x41b2, + 0x41b2, + 0x5b01, + 0x5b01, + 0x50b1, + 0x50b1, + 0x5692, + 0x5692, + 0x5a42, + 0x5a42, + 0x5a32, + 0x5a32, + 0x53a2, + 0x53a2, + 0x5952, + 0x5952, + 0x5592, + 0x5592, + 0x4a22, + 0x4a22, + 0x4a22, + 0x4a22, + 0x42a2, + 0x42a2, + 0x42a2, + 0x42a2, + 0xf005, + 0x4a12, + 0x4a12, + 0x41a2, + 0x41a2, + 0x5a01, + 0x5862, + 0x40a1, + 0x40a1, + 0x5682, + 0x5942, + 0x4392, + 0x4392, + 0x5932, + 0x5852, + 0x5582, + 0x5762, + 0x4922, + 0x4922, + 0x4292, + 0x4292, + 0x5752, + 0x5572, + 0x4832, + 0x4832, + 0x4382, + 0x4382, + 0x5662, + 0x5742, + 0x5472, + 0x5652, + 0x5562, + 0x5372, + 0xf005, + 0x3912, + 0x3912, + 0x3912, + 0x3912, + 0x3192, + 0x3192, + 0x3192, + 0x3192, + 0x4901, + 0x4901, + 0x4091, + 0x4091, + 0x4842, + 0x4842, + 0x4482, + 0x4482, + 0x4272, + 0x4272, + 0x5642, + 0x5462, + 0x3822, + 0x3822, + 0x3822, + 0x3822, + 0x3282, + 0x3282, + 0x3282, + 0x3282, + 0x3812, + 0x3812, + 0x3812, + 0x3812, + 0xf004, + 0x4732, + 0x4722, + 0x3712, + 0x3712, + 0x3172, + 0x3172, + 0x4552, + 0x4701, + 0x4071, + 0x4632, + 0x4362, + 0x4542, + 0x4452, + 0x4622, + 0x4262, + 0x4532, + 0xf003, + 0x2182, + 0x2182, + 0x3801, + 0x3081, + 0x3612, + 0x3162, + 0x3601, + 0x3061, + 0xf004, + 0x4352, + 0x4442, + 0x3522, + 0x3522, + 0x3252, + 0x3252, + 0x3501, + 0x3501, + 0x2512, + 0x2512, + 0x2512, + 0x2512, + 0x2152, + 0x2152, + 0x2152, + 0x2152, + 0xf003, + 0x3432, + 0x3342, + 0x3051, + 0x3422, + 0x3242, + 0x3332, + 0x2412, + 0x2412, + 0xf002, + 0x1142, + 0x1142, + 0x2401, + 0x2041, + 0xf002, + 0x2322, + 0x2232, + 0x1312, + 0x1312, + 0xf001, + 0x1132, + 0x1301, + 0xf001, + 0x1031, + 0x1222, + 0xf003, + 0x0082, + 0x008b, + 0x008e, + 0x0091, + 0x0094, + 0x0097, + 0x3ce2, + 0x3dd2, + 0xf003, + 0x0093, + 0x3eb2, + 0x3be2, + 0x3f92, + 0x39f2, + 0x3ae2, + 0x3db2, + 0x3bd2, + 0xf003, + 0x3f82, + 0x38f2, + 0x3cc2, + 0x008d, + 0x3e82, + 0x0090, + 0x27f2, + 0x27f2, + 0xf003, + 0x2ad2, + 0x2ad2, + 0x3da2, + 0x3cb2, + 0x3bc2, + 0x36f2, + 0x2f62, + 0x2f62, + 0xf002, + 0x28e2, + 0x2f52, + 0x2d92, + 0x29d2, + 0xf002, + 0x25f2, + 0x27e2, + 0x2ca2, + 0x2bb2, + 0xf003, + 0x2f42, + 0x2f42, + 0x24f2, + 0x24f2, + 0x3ac2, + 0x36e2, + 0x23f2, + 0x23f2, + 0xf002, + 0x1f32, + 0x1f32, + 0x2d82, + 0x28d2, + 0xf001, + 0x1f22, + 0x12f2, + 0xf002, + 0x2e62, + 0x2c92, + 0x1f01, + 0x1f01, + 0xf002, + 0x29c2, + 0x2e52, + 0x1ba2, + 0x1ba2, + 0xf002, + 0x2d72, + 0x27d2, + 0x1e42, + 0x1e42, + 0xf002, + 0x28c2, + 0x26d2, + 0x1e32, + 0x1e32, + 0xf002, + 0x19b2, + 0x19b2, + 0x2b92, + 0x2aa2, + 0xf001, + 0x1ab2, + 0x15e2, + 0xf001, + 0x14e2, + 0x1c82, + 0xf001, + 0x1d62, + 0x13e2, + 0xf001, + 0x1e22, + 0x1e01, + 0xf001, + 0x10e1, + 0x1d52, + 0xf001, + 0x15d2, + 0x1c72, + 0xf001, + 0x17c2, + 0x1d42, + 0xf001, + 0x1b82, + 0x18b2, + 0xf001, + 0x14d2, + 0x1a92, + 0xf001, + 0x19a2, + 0x1c62, + 0xf001, + 0x13d2, + 0x1b72, + 0xf001, + 0x1c52, + 0x15c2, + 0xf001, + 0x1992, + 0x1a72, + 0xf001, + 0x17a2, + 0x1792, + 0xf003, + 0x0023, + 0x3df2, + 0x2de2, + 0x2de2, + 0x1ff2, + 0x1ff2, + 0x1ff2, + 0x1ff2, + 0xf001, + 0x1fe2, + 0x1fd2, + 0xf001, + 0x1ee2, + 0x1fc2, + 0xf001, + 0x1ed2, + 0x1fb2, + 0xf001, + 0x1bf2, + 0x1ec2, + 0xf002, + 0x1cd2, + 0x1cd2, + 0x2fa2, + 0x29e2, + 0xf001, + 0x1af2, + 0x1dc2, + 0xf001, + 0x1ea2, + 0x1e92, + 0xf001, + 0x1f72, + 0x1e72, + 0xf001, + 0x1ef2, + 0x1cf2, + + /* huffTable15[580] */ + 0xf008, + 0x0101, + 0x0122, + 0x0143, + 0x0154, + 0x0165, + 0x0176, + 0x017f, + 0x0188, + 0x0199, + 0x01a2, + 0x01ab, + 0x01b4, + 0x01bd, + 0x01c2, + 0x01cb, + 0x01d4, + 0x01d9, + 0x01de, + 0x01e3, + 0x01e8, + 0x01ed, + 0x01f2, + 0x01f7, + 0x01fc, + 0x0201, + 0x0204, + 0x0207, + 0x020a, + 0x020f, + 0x0212, + 0x0215, + 0x021a, + 0x021d, + 0x0220, + 0x8192, + 0x0223, + 0x0226, + 0x0229, + 0x022c, + 0x022f, + 0x8822, + 0x8282, + 0x8812, + 0x8182, + 0x0232, + 0x0235, + 0x0238, + 0x023b, + 0x8722, + 0x8272, + 0x8462, + 0x8712, + 0x8552, + 0x8172, + 0x023e, + 0x8632, + 0x8362, + 0x8542, + 0x8452, + 0x8622, + 0x8262, + 0x8612, + 0x0241, + 0x8532, + 0x7162, + 0x7162, + 0x8352, + 0x8442, + 0x7522, + 0x7522, + 0x7252, + 0x7252, + 0x7512, + 0x7512, + 0x7152, + 0x7152, + 0x8501, + 0x8051, + 0x7432, + 0x7432, + 0x7342, + 0x7342, + 0x7422, + 0x7422, + 0x7242, + 0x7242, + 0x7332, + 0x7332, + 0x6142, + 0x6142, + 0x6142, + 0x6142, + 0x7412, + 0x7412, + 0x7401, + 0x7401, + 0x6322, + 0x6322, + 0x6322, + 0x6322, + 0x6232, + 0x6232, + 0x6232, + 0x6232, + 0x7041, + 0x7041, + 0x7301, + 0x7301, + 0x6312, + 0x6312, + 0x6312, + 0x6312, + 0x6132, + 0x6132, + 0x6132, + 0x6132, + 0x6031, + 0x6031, + 0x6031, + 0x6031, + 0x5222, + 0x5222, + 0x5222, + 0x5222, + 0x5222, + 0x5222, + 0x5222, + 0x5222, + 0x5212, + 0x5212, + 0x5212, + 0x5212, + 0x5212, + 0x5212, + 0x5212, + 0x5212, + 0x5122, + 0x5122, + 0x5122, + 0x5122, + 0x5122, + 0x5122, + 0x5122, + 0x5122, + 0x5201, + 0x5201, + 0x5201, + 0x5201, + 0x5201, + 0x5201, + 0x5201, + 0x5201, + 0x5021, + 0x5021, + 0x5021, + 0x5021, + 0x5021, + 0x5021, + 0x5021, + 0x5021, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x3112, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x4011, + 0x4011, + 0x4011, + 0x4011, + 0x4011, + 0x4011, + 0x4011, + 0x4011, + 0x4011, + 0x4011, + 0x4011, + 0x4011, + 0x4011, + 0x4011, + 0x4011, + 0x4011, + 0x3000, + 0x3000, + 0x3000, + 0x3000, + 0x3000, + 0x3000, + 0x3000, + 0x3000, + 0x3000, + 0x3000, + 0x3000, + 0x3000, + 0x3000, + 0x3000, + 0x3000, + 0x3000, + 0x3000, + 0x3000, + 0x3000, + 0x3000, + 0x3000, + 0x3000, + 0x3000, + 0x3000, + 0x3000, + 0x3000, + 0x3000, + 0x3000, + 0x3000, + 0x3000, + 0x3000, + 0x3000, + 0xf005, + 0x5ff2, + 0x5fe2, + 0x5ef2, + 0x5fd2, + 0x4ee2, + 0x4ee2, + 0x5df2, + 0x5fc2, + 0x5cf2, + 0x5ed2, + 0x5de2, + 0x5fb2, + 0x4bf2, + 0x4bf2, + 0x5ec2, + 0x5ce2, + 0x4dd2, + 0x4dd2, + 0x4fa2, + 0x4fa2, + 0x4af2, + 0x4af2, + 0x4eb2, + 0x4eb2, + 0x4be2, + 0x4be2, + 0x4dc2, + 0x4dc2, + 0x4cd2, + 0x4cd2, + 0x4f92, + 0x4f92, + 0xf005, + 0x49f2, + 0x49f2, + 0x4ae2, + 0x4ae2, + 0x4db2, + 0x4db2, + 0x4bd2, + 0x4bd2, + 0x4f82, + 0x4f82, + 0x48f2, + 0x48f2, + 0x4cc2, + 0x4cc2, + 0x4e92, + 0x4e92, + 0x49e2, + 0x49e2, + 0x4f72, + 0x4f72, + 0x47f2, + 0x47f2, + 0x4da2, + 0x4da2, + 0x4ad2, + 0x4ad2, + 0x4cb2, + 0x4cb2, + 0x4f62, + 0x4f62, + 0x5ea2, + 0x5f01, + 0xf004, + 0x3bc2, + 0x3bc2, + 0x36f2, + 0x36f2, + 0x4e82, + 0x48e2, + 0x4f52, + 0x4d92, + 0x35f2, + 0x35f2, + 0x3e72, + 0x3e72, + 0x37e2, + 0x37e2, + 0x3ca2, + 0x3ca2, + 0xf004, + 0x3ac2, + 0x3ac2, + 0x3bb2, + 0x3bb2, + 0x49d2, + 0x4d82, + 0x3f42, + 0x3f42, + 0x34f2, + 0x34f2, + 0x3f32, + 0x3f32, + 0x33f2, + 0x33f2, + 0x38d2, + 0x38d2, + 0xf004, + 0x36e2, + 0x36e2, + 0x3f22, + 0x3f22, + 0x32f2, + 0x32f2, + 0x4e62, + 0x40f1, + 0x3f12, + 0x3f12, + 0x31f2, + 0x31f2, + 0x3c92, + 0x3c92, + 0x39c2, + 0x39c2, + 0xf003, + 0x3e52, + 0x3ba2, + 0x3ab2, + 0x35e2, + 0x3d72, + 0x37d2, + 0x3e42, + 0x34e2, + 0xf003, + 0x3c82, + 0x38c2, + 0x3e32, + 0x3d62, + 0x36d2, + 0x33e2, + 0x3b92, + 0x39b2, + 0xf004, + 0x3e22, + 0x3e22, + 0x3aa2, + 0x3aa2, + 0x32e2, + 0x32e2, + 0x3e12, + 0x3e12, + 0x31e2, + 0x31e2, + 0x4e01, + 0x40e1, + 0x3d52, + 0x3d52, + 0x35d2, + 0x35d2, + 0xf003, + 0x3c72, + 0x37c2, + 0x3d42, + 0x3b82, + 0x24d2, + 0x24d2, + 0x38b2, + 0x3a92, + 0xf003, + 0x39a2, + 0x3c62, + 0x36c2, + 0x3d32, + 0x23d2, + 0x23d2, + 0x22d2, + 0x22d2, + 0xf003, + 0x3d22, + 0x3d01, + 0x2d12, + 0x2d12, + 0x2b72, + 0x2b72, + 0x27b2, + 0x27b2, + 0xf003, + 0x21d2, + 0x21d2, + 0x3c52, + 0x30d1, + 0x25c2, + 0x25c2, + 0x2a82, + 0x2a82, + 0xf002, + 0x28a2, + 0x2c42, + 0x24c2, + 0x2b62, + 0xf003, + 0x26b2, + 0x26b2, + 0x3992, + 0x3c01, + 0x2c32, + 0x2c32, + 0x23c2, + 0x23c2, + 0xf003, + 0x2a72, + 0x2a72, + 0x27a2, + 0x27a2, + 0x26a2, + 0x26a2, + 0x30c1, + 0x3b01, + 0xf002, + 0x12c2, + 0x12c2, + 0x2c22, + 0x2b52, + 0xf002, + 0x25b2, + 0x2c12, + 0x2982, + 0x2892, + 0xf002, + 0x21c2, + 0x2b42, + 0x24b2, + 0x2a62, + 0xf002, + 0x2b32, + 0x2972, + 0x13b2, + 0x13b2, + 0xf002, + 0x2792, + 0x2882, + 0x2b22, + 0x2a52, + 0xf002, + 0x12b2, + 0x12b2, + 0x25a2, + 0x2b12, + 0xf002, + 0x11b2, + 0x11b2, + 0x20b1, + 0x2962, + 0xf002, + 0x2692, + 0x2a42, + 0x24a2, + 0x2872, + 0xf002, + 0x2782, + 0x2a32, + 0x13a2, + 0x13a2, + 0xf001, + 0x1952, + 0x1592, + 0xf001, + 0x1a22, + 0x12a2, + 0xf001, + 0x1a12, + 0x11a2, + 0xf002, + 0x2a01, + 0x20a1, + 0x1862, + 0x1862, + 0xf001, + 0x1682, + 0x1942, + 0xf001, + 0x1492, + 0x1932, + 0xf002, + 0x1392, + 0x1392, + 0x2772, + 0x2901, + 0xf001, + 0x1852, + 0x1582, + 0xf001, + 0x1922, + 0x1762, + 0xf001, + 0x1672, + 0x1292, + 0xf001, + 0x1912, + 0x1091, + 0xf001, + 0x1842, + 0x1482, + 0xf001, + 0x1752, + 0x1572, + 0xf001, + 0x1832, + 0x1382, + 0xf001, + 0x1662, + 0x1742, + 0xf001, + 0x1472, + 0x1801, + 0xf001, + 0x1081, + 0x1652, + 0xf001, + 0x1562, + 0x1732, + 0xf001, + 0x1372, + 0x1642, + 0xf001, + 0x1701, + 0x1071, + 0xf001, + 0x1601, + 0x1061, + + /* huffTable16[651] */ + 0xf008, + 0x0101, + 0x010a, + 0x0113, + 0x8ff2, + 0x0118, + 0x011d, + 0x0120, + 0x82f2, + 0x0131, + 0x8f12, + 0x81f2, + 0x0134, + 0x0145, + 0x0156, + 0x0167, + 0x0178, + 0x0189, + 0x019a, + 0x01a3, + 0x01ac, + 0x01b5, + 0x01be, + 0x01c7, + 0x01d0, + 0x01d9, + 0x01de, + 0x01e3, + 0x01e6, + 0x01eb, + 0x01f0, + 0x8152, + 0x01f3, + 0x01f6, + 0x01f9, + 0x01fc, + 0x8412, + 0x8142, + 0x01ff, + 0x8322, + 0x8232, + 0x7312, + 0x7312, + 0x7132, + 0x7132, + 0x8301, + 0x8031, + 0x7222, + 0x7222, + 0x6212, + 0x6212, + 0x6212, + 0x6212, + 0x6122, + 0x6122, + 0x6122, + 0x6122, + 0x6201, + 0x6201, + 0x6201, + 0x6201, + 0x6021, + 0x6021, + 0x6021, + 0x6021, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x3011, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0x1000, + 0xf003, + 0x3fe2, + 0x3ef2, + 0x3fd2, + 0x3df2, + 0x3fc2, + 0x3cf2, + 0x3fb2, + 0x3bf2, + 0xf003, + 0x2fa2, + 0x2fa2, + 0x3af2, + 0x3f92, + 0x39f2, + 0x38f2, + 0x2f82, + 0x2f82, + 0xf002, + 0x2f72, + 0x27f2, + 0x2f62, + 0x26f2, + 0xf002, + 0x2f52, + 0x25f2, + 0x1f42, + 0x1f42, + 0xf001, + 0x14f2, + 0x13f2, + 0xf004, + 0x10f1, + 0x10f1, + 0x10f1, + 0x10f1, + 0x10f1, + 0x10f1, + 0x10f1, + 0x10f1, + 0x2f32, + 0x2f32, + 0x2f32, + 0x2f32, + 0x00e2, + 0x00f3, + 0x00fc, + 0x0105, + 0xf001, + 0x1f22, + 0x1f01, + 0xf004, + 0x00fa, + 0x00ff, + 0x0104, + 0x0109, + 0x010c, + 0x0111, + 0x0116, + 0x0119, + 0x011e, + 0x0123, + 0x0128, + 0x43e2, + 0x012d, + 0x0130, + 0x0133, + 0x0136, + 0xf004, + 0x0128, + 0x012b, + 0x012e, + 0x4d01, + 0x0131, + 0x0134, + 0x0137, + 0x4c32, + 0x013a, + 0x4c12, + 0x40c1, + 0x013d, + 0x32e2, + 0x32e2, + 0x4e22, + 0x4e12, + 0xf004, + 0x43d2, + 0x4d22, + 0x42d2, + 0x41d2, + 0x4b32, + 0x012f, + 0x3d12, + 0x3d12, + 0x44c2, + 0x4b62, + 0x43c2, + 0x47a2, + 0x3c22, + 0x3c22, + 0x42c2, + 0x45b2, + 0xf004, + 0x41c2, + 0x4c01, + 0x4b42, + 0x44b2, + 0x4a62, + 0x46a2, + 0x33b2, + 0x33b2, + 0x4a52, + 0x45a2, + 0x3b22, + 0x3b22, + 0x32b2, + 0x32b2, + 0x3b12, + 0x3b12, + 0xf004, + 0x31b2, + 0x31b2, + 0x4b01, + 0x40b1, + 0x4962, + 0x4692, + 0x4a42, + 0x44a2, + 0x4872, + 0x4782, + 0x33a2, + 0x33a2, + 0x4a32, + 0x4952, + 0x3a22, + 0x3a22, + 0xf004, + 0x4592, + 0x4862, + 0x31a2, + 0x31a2, + 0x4682, + 0x4772, + 0x3492, + 0x3492, + 0x4942, + 0x4752, + 0x3762, + 0x3762, + 0x22a2, + 0x22a2, + 0x22a2, + 0x22a2, + 0xf003, + 0x2a12, + 0x2a12, + 0x3a01, + 0x30a1, + 0x3932, + 0x3392, + 0x3852, + 0x3582, + 0xf003, + 0x2922, + 0x2922, + 0x2292, + 0x2292, + 0x3672, + 0x3901, + 0x2912, + 0x2912, + 0xf003, + 0x2192, + 0x2192, + 0x3091, + 0x3842, + 0x3482, + 0x3572, + 0x3832, + 0x3382, + 0xf003, + 0x3662, + 0x3822, + 0x2282, + 0x2282, + 0x3742, + 0x3472, + 0x2812, + 0x2812, + 0xf003, + 0x2182, + 0x2182, + 0x2081, + 0x2081, + 0x3801, + 0x3652, + 0x2732, + 0x2732, + 0xf003, + 0x2372, + 0x2372, + 0x3562, + 0x3642, + 0x2722, + 0x2722, + 0x2272, + 0x2272, + 0xf003, + 0x3462, + 0x3552, + 0x2701, + 0x2701, + 0x1712, + 0x1712, + 0x1712, + 0x1712, + 0xf002, + 0x1172, + 0x1172, + 0x2071, + 0x2632, + 0xf002, + 0x2362, + 0x2542, + 0x2452, + 0x2622, + 0xf001, + 0x1262, + 0x1612, + 0xf002, + 0x1162, + 0x1162, + 0x2601, + 0x2061, + 0xf002, + 0x1352, + 0x1352, + 0x2532, + 0x2442, + 0xf001, + 0x1522, + 0x1252, + 0xf001, + 0x1512, + 0x1501, + 0xf001, + 0x1432, + 0x1342, + 0xf001, + 0x1051, + 0x1422, + 0xf001, + 0x1242, + 0x1332, + 0xf001, + 0x1401, + 0x1041, + 0xf004, + 0x4ec2, + 0x0086, + 0x3ed2, + 0x3ed2, + 0x39e2, + 0x39e2, + 0x4ae2, + 0x49d2, + 0x2ee2, + 0x2ee2, + 0x2ee2, + 0x2ee2, + 0x3de2, + 0x3de2, + 0x3be2, + 0x3be2, + 0xf003, + 0x2eb2, + 0x2eb2, + 0x2dc2, + 0x2dc2, + 0x3cd2, + 0x3bd2, + 0x2ea2, + 0x2ea2, + 0xf003, + 0x2cc2, + 0x2cc2, + 0x3da2, + 0x3ad2, + 0x3e72, + 0x3ca2, + 0x2ac2, + 0x2ac2, + 0xf003, + 0x39c2, + 0x3d72, + 0x2e52, + 0x2e52, + 0x1db2, + 0x1db2, + 0x1db2, + 0x1db2, + 0xf002, + 0x1e92, + 0x1e92, + 0x2cb2, + 0x2bc2, + 0xf002, + 0x2e82, + 0x28e2, + 0x2d92, + 0x27e2, + 0xf002, + 0x2bb2, + 0x2d82, + 0x28d2, + 0x2e62, + 0xf001, + 0x16e2, + 0x1c92, + 0xf002, + 0x2ba2, + 0x2ab2, + 0x25e2, + 0x27d2, + 0xf002, + 0x1e42, + 0x1e42, + 0x24e2, + 0x2c82, + 0xf001, + 0x18c2, + 0x1e32, + 0xf002, + 0x1d62, + 0x1d62, + 0x26d2, + 0x2b92, + 0xf002, + 0x29b2, + 0x2aa2, + 0x11e2, + 0x11e2, + 0xf002, + 0x14d2, + 0x14d2, + 0x28b2, + 0x29a2, + 0xf002, + 0x1b72, + 0x1b72, + 0x27b2, + 0x20d1, + 0xf001, + 0x1e01, + 0x10e1, + 0xf001, + 0x1d52, + 0x15d2, + 0xf001, + 0x1c72, + 0x17c2, + 0xf001, + 0x1d42, + 0x1b82, + 0xf001, + 0x1a92, + 0x1c62, + 0xf001, + 0x16c2, + 0x1d32, + 0xf001, + 0x1c52, + 0x15c2, + 0xf001, + 0x1a82, + 0x18a2, + 0xf001, + 0x1992, + 0x1c42, + 0xf001, + 0x16b2, + 0x1a72, + 0xf001, + 0x1b52, + 0x1982, + 0xf001, + 0x1892, + 0x1972, + 0xf001, + 0x1792, + 0x1882, + 0xf001, + 0x1ce2, + 0x1dd2, + + /* huffTable24[705] */ + 0xf009, + 0x8fe2, + 0x8fe2, + 0x8ef2, + 0x8ef2, + 0x8fd2, + 0x8fd2, + 0x8df2, + 0x8df2, + 0x8fc2, + 0x8fc2, + 0x8cf2, + 0x8cf2, + 0x8fb2, + 0x8fb2, + 0x8bf2, + 0x8bf2, + 0x7af2, + 0x7af2, + 0x7af2, + 0x7af2, + 0x8fa2, + 0x8fa2, + 0x8f92, + 0x8f92, + 0x79f2, + 0x79f2, + 0x79f2, + 0x79f2, + 0x78f2, + 0x78f2, + 0x78f2, + 0x78f2, + 0x8f82, + 0x8f82, + 0x8f72, + 0x8f72, + 0x77f2, + 0x77f2, + 0x77f2, + 0x77f2, + 0x7f62, + 0x7f62, + 0x7f62, + 0x7f62, + 0x76f2, + 0x76f2, + 0x76f2, + 0x76f2, + 0x7f52, + 0x7f52, + 0x7f52, + 0x7f52, + 0x75f2, + 0x75f2, + 0x75f2, + 0x75f2, + 0x7f42, + 0x7f42, + 0x7f42, + 0x7f42, + 0x74f2, + 0x74f2, + 0x74f2, + 0x74f2, + 0x7f32, + 0x7f32, + 0x7f32, + 0x7f32, + 0x73f2, + 0x73f2, + 0x73f2, + 0x73f2, + 0x7f22, + 0x7f22, + 0x7f22, + 0x7f22, + 0x72f2, + 0x72f2, + 0x72f2, + 0x72f2, + 0x71f2, + 0x71f2, + 0x71f2, + 0x71f2, + 0x8f12, + 0x8f12, + 0x80f1, + 0x80f1, + 0x9f01, + 0x0201, + 0x0206, + 0x020b, + 0x0210, + 0x0215, + 0x021a, + 0x021f, + 0x4ff2, + 0x4ff2, + 0x4ff2, + 0x4ff2, + 0x4ff2, + 0x4ff2, + 0x4ff2, + 0x4ff2, + 0x4ff2, + 0x4ff2, + 0x4ff2, + 0x4ff2, + 0x4ff2, + 0x4ff2, + 0x4ff2, + 0x4ff2, + 0x4ff2, + 0x4ff2, + 0x4ff2, + 0x4ff2, + 0x4ff2, + 0x4ff2, + 0x4ff2, + 0x4ff2, + 0x4ff2, + 0x4ff2, + 0x4ff2, + 0x4ff2, + 0x4ff2, + 0x4ff2, + 0x4ff2, + 0x4ff2, + 0x0224, + 0x0229, + 0x0232, + 0x0237, + 0x023a, + 0x023f, + 0x0242, + 0x0245, + 0x024a, + 0x024d, + 0x0250, + 0x0253, + 0x0256, + 0x0259, + 0x025c, + 0x025f, + 0x0262, + 0x0265, + 0x0268, + 0x026b, + 0x026e, + 0x0271, + 0x0274, + 0x0277, + 0x027a, + 0x027d, + 0x0280, + 0x0283, + 0x0288, + 0x028b, + 0x028e, + 0x0291, + 0x0294, + 0x0297, + 0x029a, + 0x029f, + 0x94b2, + 0x02a4, + 0x02a7, + 0x02aa, + 0x93b2, + 0x9882, + 0x02af, + 0x92b2, + 0x02b2, + 0x02b5, + 0x9692, + 0x94a2, + 0x02b8, + 0x9782, + 0x9a32, + 0x93a2, + 0x9952, + 0x9592, + 0x9a22, + 0x92a2, + 0x91a2, + 0x9862, + 0x9682, + 0x9772, + 0x9942, + 0x9492, + 0x9932, + 0x9392, + 0x9852, + 0x9582, + 0x9922, + 0x9762, + 0x9672, + 0x9292, + 0x9912, + 0x9192, + 0x9842, + 0x9482, + 0x9752, + 0x9572, + 0x9832, + 0x9382, + 0x9662, + 0x9822, + 0x9282, + 0x9812, + 0x9742, + 0x9472, + 0x9182, + 0x02bb, + 0x9652, + 0x9562, + 0x9712, + 0x02be, + 0x8372, + 0x8372, + 0x9732, + 0x9722, + 0x8272, + 0x8272, + 0x8642, + 0x8642, + 0x8462, + 0x8462, + 0x8552, + 0x8552, + 0x8172, + 0x8172, + 0x8632, + 0x8632, + 0x8362, + 0x8362, + 0x8542, + 0x8542, + 0x8452, + 0x8452, + 0x8622, + 0x8622, + 0x8262, + 0x8262, + 0x8612, + 0x8612, + 0x8162, + 0x8162, + 0x9601, + 0x9061, + 0x8532, + 0x8532, + 0x8352, + 0x8352, + 0x8442, + 0x8442, + 0x8522, + 0x8522, + 0x8252, + 0x8252, + 0x8512, + 0x8512, + 0x9501, + 0x9051, + 0x7152, + 0x7152, + 0x7152, + 0x7152, + 0x8432, + 0x8432, + 0x8342, + 0x8342, + 0x7422, + 0x7422, + 0x7422, + 0x7422, + 0x7242, + 0x7242, + 0x7242, + 0x7242, + 0x7332, + 0x7332, + 0x7332, + 0x7332, + 0x7412, + 0x7412, + 0x7412, + 0x7412, + 0x7142, + 0x7142, + 0x7142, + 0x7142, + 0x8401, + 0x8401, + 0x8041, + 0x8041, + 0x7322, + 0x7322, + 0x7322, + 0x7322, + 0x7232, + 0x7232, + 0x7232, + 0x7232, + 0x6312, + 0x6312, + 0x6312, + 0x6312, + 0x6312, + 0x6312, + 0x6312, + 0x6312, + 0x6132, + 0x6132, + 0x6132, + 0x6132, + 0x6132, + 0x6132, + 0x6132, + 0x6132, + 0x7301, + 0x7301, + 0x7301, + 0x7301, + 0x7031, + 0x7031, + 0x7031, + 0x7031, + 0x6222, + 0x6222, + 0x6222, + 0x6222, + 0x6222, + 0x6222, + 0x6222, + 0x6222, + 0x5212, + 0x5212, + 0x5212, + 0x5212, + 0x5212, + 0x5212, + 0x5212, + 0x5212, + 0x5212, + 0x5212, + 0x5212, + 0x5212, + 0x5212, + 0x5212, + 0x5212, + 0x5212, + 0x5122, + 0x5122, + 0x5122, + 0x5122, + 0x5122, + 0x5122, + 0x5122, + 0x5122, + 0x5122, + 0x5122, + 0x5122, + 0x5122, + 0x5122, + 0x5122, + 0x5122, + 0x5122, + 0x6201, + 0x6201, + 0x6201, + 0x6201, + 0x6201, + 0x6201, + 0x6201, + 0x6201, + 0x6021, + 0x6021, + 0x6021, + 0x6021, + 0x6021, + 0x6021, + 0x6021, + 0x6021, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x4112, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x4101, + 0x4011, + 0x4011, + 0x4011, + 0x4011, + 0x4011, + 0x4011, + 0x4011, + 0x4011, + 0x4011, + 0x4011, + 0x4011, + 0x4011, + 0x4011, + 0x4011, + 0x4011, + 0x4011, + 0x4011, + 0x4011, + 0x4011, + 0x4011, + 0x4011, + 0x4011, + 0x4011, + 0x4011, + 0x4011, + 0x4011, + 0x4011, + 0x4011, + 0x4011, + 0x4011, + 0x4011, + 0x4011, + 0x4000, + 0x4000, + 0x4000, + 0x4000, + 0x4000, + 0x4000, + 0x4000, + 0x4000, + 0x4000, + 0x4000, + 0x4000, + 0x4000, + 0x4000, + 0x4000, + 0x4000, + 0x4000, + 0x4000, + 0x4000, + 0x4000, + 0x4000, + 0x4000, + 0x4000, + 0x4000, + 0x4000, + 0x4000, + 0x4000, + 0x4000, + 0x4000, + 0x4000, + 0x4000, + 0x4000, + 0x4000, + 0xf002, + 0x2ee2, + 0x2ed2, + 0x2de2, + 0x2ec2, + 0xf002, + 0x2ce2, + 0x2dd2, + 0x2eb2, + 0x2be2, + 0xf002, + 0x2dc2, + 0x2cd2, + 0x2ea2, + 0x2ae2, + 0xf002, + 0x2db2, + 0x2bd2, + 0x2cc2, + 0x2e92, + 0xf002, + 0x29e2, + 0x2da2, + 0x2ad2, + 0x2cb2, + 0xf002, + 0x2bc2, + 0x2e82, + 0x28e2, + 0x2d92, + 0xf002, + 0x29d2, + 0x2e72, + 0x27e2, + 0x2ca2, + 0xf002, + 0x2ac2, + 0x2bb2, + 0x2d82, + 0x28d2, + 0xf003, + 0x3e01, + 0x30e1, + 0x2d01, + 0x2d01, + 0x16e2, + 0x16e2, + 0x16e2, + 0x16e2, + 0xf002, + 0x2e62, + 0x2c92, + 0x19c2, + 0x19c2, + 0xf001, + 0x1e52, + 0x1ab2, + 0xf002, + 0x15e2, + 0x15e2, + 0x2ba2, + 0x2d72, + 0xf001, + 0x17d2, + 0x14e2, + 0xf001, + 0x1c82, + 0x18c2, + 0xf002, + 0x2e42, + 0x2e22, + 0x1e32, + 0x1e32, + 0xf001, + 0x1d62, + 0x16d2, + 0xf001, + 0x13e2, + 0x1b92, + 0xf001, + 0x19b2, + 0x1aa2, + 0xf001, + 0x12e2, + 0x1e12, + 0xf001, + 0x11e2, + 0x1d52, + 0xf001, + 0x15d2, + 0x1c72, + 0xf001, + 0x17c2, + 0x1d42, + 0xf001, + 0x1b82, + 0x18b2, + 0xf001, + 0x14d2, + 0x1a92, + 0xf001, + 0x19a2, + 0x1c62, + 0xf001, + 0x16c2, + 0x1d32, + 0xf001, + 0x13d2, + 0x1d22, + 0xf001, + 0x12d2, + 0x1d12, + 0xf001, + 0x1b72, + 0x17b2, + 0xf001, + 0x11d2, + 0x1c52, + 0xf001, + 0x15c2, + 0x1a82, + 0xf001, + 0x18a2, + 0x1992, + 0xf001, + 0x1c42, + 0x14c2, + 0xf001, + 0x1b62, + 0x16b2, + 0xf002, + 0x20d1, + 0x2c01, + 0x1c32, + 0x1c32, + 0xf001, + 0x13c2, + 0x1a72, + 0xf001, + 0x17a2, + 0x1c22, + 0xf001, + 0x12c2, + 0x1b52, + 0xf001, + 0x15b2, + 0x1c12, + 0xf001, + 0x1982, + 0x1892, + 0xf001, + 0x11c2, + 0x1b42, + 0xf002, + 0x20c1, + 0x2b01, + 0x1b32, + 0x1b32, + 0xf002, + 0x20b1, + 0x2a01, + 0x1a12, + 0x1a12, + 0xf001, + 0x1a62, + 0x16a2, + 0xf001, + 0x1972, + 0x1792, + 0xf002, + 0x20a1, + 0x2901, + 0x1091, + 0x1091, + 0xf001, + 0x1b22, + 0x1a52, + 0xf001, + 0x15a2, + 0x1b12, + 0xf001, + 0x11b2, + 0x1962, + 0xf001, + 0x1a42, + 0x1872, + 0xf001, + 0x1801, + 0x1081, + 0xf001, + 0x1701, + 0x1071, +}; + +#define HUFF_OFFSET_01 0 +#define HUFF_OFFSET_02 (9 + HUFF_OFFSET_01) +#define HUFF_OFFSET_03 (65 + HUFF_OFFSET_02) +#define HUFF_OFFSET_05 (65 + HUFF_OFFSET_03) +#define HUFF_OFFSET_06 (257 + HUFF_OFFSET_05) +#define HUFF_OFFSET_07 (129 + HUFF_OFFSET_06) +#define HUFF_OFFSET_08 (110 + HUFF_OFFSET_07) +#define HUFF_OFFSET_09 (280 + HUFF_OFFSET_08) +#define HUFF_OFFSET_10 (93 + HUFF_OFFSET_09) +#define HUFF_OFFSET_11 (320 + HUFF_OFFSET_10) +#define HUFF_OFFSET_12 (296 + HUFF_OFFSET_11) +#define HUFF_OFFSET_13 (185 + HUFF_OFFSET_12) +#define HUFF_OFFSET_15 (497 + HUFF_OFFSET_13) +#define HUFF_OFFSET_16 (580 + HUFF_OFFSET_15) +#define HUFF_OFFSET_24 (651 + HUFF_OFFSET_16) + +const int huffTabOffset[HUFF_PAIRTABS] = { + 0, + HUFF_OFFSET_01, + HUFF_OFFSET_02, + HUFF_OFFSET_03, + 0, + HUFF_OFFSET_05, + HUFF_OFFSET_06, + HUFF_OFFSET_07, + HUFF_OFFSET_08, + HUFF_OFFSET_09, + HUFF_OFFSET_10, + HUFF_OFFSET_11, + HUFF_OFFSET_12, + HUFF_OFFSET_13, + 0, + HUFF_OFFSET_15, + HUFF_OFFSET_16, + HUFF_OFFSET_16, + HUFF_OFFSET_16, + HUFF_OFFSET_16, + HUFF_OFFSET_16, + HUFF_OFFSET_16, + HUFF_OFFSET_16, + HUFF_OFFSET_16, + HUFF_OFFSET_24, + HUFF_OFFSET_24, + HUFF_OFFSET_24, + HUFF_OFFSET_24, + HUFF_OFFSET_24, + HUFF_OFFSET_24, + HUFF_OFFSET_24, + HUFF_OFFSET_24, +}; + +const HuffTabLookup huffTabLookup[HUFF_PAIRTABS] = { + {0, noBits}, {0, oneShot}, {0, oneShot}, {0, oneShot}, {0, invalidTab}, + {0, oneShot}, {0, oneShot}, {0, loopNoLinbits}, {0, loopNoLinbits}, {0, loopNoLinbits}, + {0, loopNoLinbits}, {0, loopNoLinbits}, {0, loopNoLinbits}, {0, loopNoLinbits}, {0, invalidTab}, + {0, loopNoLinbits}, {1, loopLinbits}, {2, loopLinbits}, {3, loopLinbits}, {4, loopLinbits}, + {6, loopLinbits}, {8, loopLinbits}, {10, loopLinbits}, {13, loopLinbits}, {4, loopLinbits}, + {5, loopLinbits}, {6, loopLinbits}, {7, loopLinbits}, {8, loopLinbits}, {9, loopLinbits}, + {11, loopLinbits}, {13, loopLinbits}, +}; + +/* tables for quadruples + * format 0xAB + * A = length of codeword + * B = codeword + */ +const unsigned char quadTable[64 + 16] = { + /* table A */ + 0x6b, + 0x6f, + 0x6d, + 0x6e, + 0x67, + 0x65, + 0x59, + 0x59, + 0x56, + 0x56, + 0x53, + 0x53, + 0x5a, + 0x5a, + 0x5c, + 0x5c, + 0x42, + 0x42, + 0x42, + 0x42, + 0x41, + 0x41, + 0x41, + 0x41, + 0x44, + 0x44, + 0x44, + 0x44, + 0x48, + 0x48, + 0x48, + 0x48, + 0x10, + 0x10, + 0x10, + 0x10, + 0x10, + 0x10, + 0x10, + 0x10, + 0x10, + 0x10, + 0x10, + 0x10, + 0x10, + 0x10, + 0x10, + 0x10, + 0x10, + 0x10, + 0x10, + 0x10, + 0x10, + 0x10, + 0x10, + 0x10, + 0x10, + 0x10, + 0x10, + 0x10, + 0x10, + 0x10, + 0x10, + 0x10, + /* table B */ + 0x4f, + 0x4e, + 0x4d, + 0x4c, + 0x4b, + 0x4a, + 0x49, + 0x48, + 0x47, + 0x46, + 0x45, + 0x44, + 0x43, + 0x42, + 0x41, + 0x40, +}; + +const int quadTabOffset[2] = {0, 64}; +const int quadTabMaxBits[2] = {6, 4}; + +/* helper macros - see comments in hufftabs.c about the format of the huffman + * tables */ +#define GetMaxbits(x) ((int) ((((unsigned short) (x)) >> 0) & 0x000f)) +#define GetHLen(x) ((int) ((((unsigned short) (x)) >> 12) & 0x000f)) +#define GetCWY(x) ((int) ((((unsigned short) (x)) >> 8) & 0x000f)) +#define GetCWX(x) ((int) ((((unsigned short) (x)) >> 4) & 0x000f)) +#define GetSignBits(x) ((int) ((((unsigned short) (x)) >> 0) & 0x000f)) + +#define GetHLenQ(x) ((int) ((((unsigned char) (x)) >> 4) & 0x0f)) +#define GetCWVQ(x) ((int) ((((unsigned char) (x)) >> 3) & 0x01)) +#define GetCWWQ(x) ((int) ((((unsigned char) (x)) >> 2) & 0x01)) +#define GetCWXQ(x) ((int) ((((unsigned char) (x)) >> 1) & 0x01)) +#define GetCWYQ(x) ((int) ((((unsigned char) (x)) >> 0) & 0x01)) + +/* apply sign of s to the positive number x (save in MSB, will do two's + * complement in dequant) */ +#define ApplySign(x, s) \ + { (x) |= ((s) & 0x80000000); } + +/************************************************************************************** + * Function: DecodeHuffmanPairs + * + * Description: decode 2-way vector Huffman codes in the "bigValues" region of + *spectrum + * + * Inputs: valid BitStreamInfo struct, pointing to start of pair-wise codes + * pointer to xy buffer to received decoded values + * number of codewords to decode + * index of Huffman table to use + * number of bits remaining in bitstream + * + * Outputs: pairs of decoded coefficients in vwxy + * updated BitStreamInfo struct + * + * Return: number of bits used, or -1 if out of bits + * + * Notes: assumes that nVals is an even number + * si_huff.bit tests every Huffman codeword in every table (though + *not necessarily all linBits outputs for x,y > 15) + **************************************************************************************/ +// no improvement with section=data +static int DecodeHuffmanPairs(int *xy, int nVals, int tabIdx, int bitsLeft, unsigned char *buf, int bitOffset) { + int i, x, y; + int cachedBits, padBits, len, startBits, linBits, maxBits, minBits; + HuffTabType tabType; + unsigned short cw, *tBase, *tCurr; + unsigned int cache; + + if (nVals <= 0) + return 0; + + if (bitsLeft < 0) + return -1; + startBits = bitsLeft; + + tBase = (unsigned short *) (huffTable + huffTabOffset[tabIdx]); + linBits = huffTabLookup[tabIdx].linBits; + tabType = huffTabLookup[tabIdx].tabType; + + ASSERT(!(nVals & 0x01)); + ASSERT(tabIdx < HUFF_PAIRTABS); + ASSERT(tabIdx >= 0); + ASSERT(tabType != invalidTab); + + /* initially fill cache with any partial byte */ + cache = 0; + cachedBits = (8 - bitOffset) & 0x07; + if (cachedBits) + cache = (unsigned int) (*buf++) << (32 - cachedBits); + bitsLeft -= cachedBits; + + if (tabType == noBits) { + /* table 0, no data, x = y = 0 */ + for (i = 0; i < nVals; i += 2) { + xy[i + 0] = 0; + xy[i + 1] = 0; + } + return 0; + } else if (tabType == oneShot) { + /* single lookup, no escapes */ + maxBits = GetMaxbits(tBase[0]); + tBase++; + padBits = 0; + while (nVals > 0) { + /* refill cache - assumes cachedBits <= 16 */ + if (bitsLeft >= 16) { + /* load 2 new bytes into left-justified cache */ + cache |= (unsigned int) (*buf++) << (24 - cachedBits); + cache |= (unsigned int) (*buf++) << (16 - cachedBits); + cachedBits += 16; + bitsLeft -= 16; + } else { + /* last time through, pad cache with zeros and drain cache */ + if (cachedBits + bitsLeft <= 0) + return -1; + if (bitsLeft > 0) + cache |= (unsigned int) (*buf++) << (24 - cachedBits); + if (bitsLeft > 8) + cache |= (unsigned int) (*buf++) << (16 - cachedBits); + cachedBits += bitsLeft; + bitsLeft = 0; + + cache &= (signed int) 0x80000000 >> (cachedBits - 1); + padBits = 11; + cachedBits += padBits; /* okay if this is > 32 (0's automatically + shifted in from right) */ + } + + /* largest maxBits = 9, plus 2 for sign bits, so make sure cache has at + * least 11 bits */ + while (nVals > 0 && cachedBits >= 11) { + cw = tBase[cache >> (32 - maxBits)]; + len = GetHLen(cw); + cachedBits -= len; + cache <<= len; + + x = GetCWX(cw); + if (x) { + ApplySign(x, cache); + cache <<= 1; + cachedBits--; + } + y = GetCWY(cw); + if (y) { + ApplySign(y, cache); + cache <<= 1; + cachedBits--; + } + + /* ran out of bits - should never have consumed padBits */ + if (cachedBits < padBits) + return -1; + + *xy++ = x; + *xy++ = y; + nVals -= 2; + } + } + bitsLeft += (cachedBits - padBits); + return (startBits - bitsLeft); + } else if (tabType == loopLinbits || tabType == loopNoLinbits) { + tCurr = tBase; + padBits = 0; + while (nVals > 0) { + /* refill cache - assumes cachedBits <= 16 */ + if (bitsLeft >= 16) { + /* load 2 new bytes into left-justified cache */ + cache |= (unsigned int) (*buf++) << (24 - cachedBits); + cache |= (unsigned int) (*buf++) << (16 - cachedBits); + cachedBits += 16; + bitsLeft -= 16; + } else { + /* last time through, pad cache with zeros and drain cache */ + if (cachedBits + bitsLeft <= 0) + return -1; + if (bitsLeft > 0) + cache |= (unsigned int) (*buf++) << (24 - cachedBits); + if (bitsLeft > 8) + cache |= (unsigned int) (*buf++) << (16 - cachedBits); + cachedBits += bitsLeft; + bitsLeft = 0; + + cache &= (signed int) 0x80000000 >> (cachedBits - 1); + padBits = 11; + cachedBits += padBits; /* okay if this is > 32 (0's automatically + shifted in from right) */ + } + + /* largest maxBits = 9, plus 2 for sign bits, so make sure cache has at + * least 11 bits */ + while (nVals > 0 && cachedBits >= 11) { + maxBits = GetMaxbits(tCurr[0]); + cw = tCurr[(cache >> (32 - maxBits)) + 1]; + len = GetHLen(cw); + if (!len) { + cachedBits -= maxBits; + cache <<= maxBits; + tCurr += cw; + continue; + } + cachedBits -= len; + cache <<= len; + + x = GetCWX(cw); + y = GetCWY(cw); + + if (x == 15 && tabType == loopLinbits) { + minBits = linBits + 1 + (y ? 1 : 0); + if (cachedBits + bitsLeft < minBits) + return -1; + while (cachedBits < minBits) { + cache |= (unsigned int) (*buf++) << (24 - cachedBits); + cachedBits += 8; + bitsLeft -= 8; + } + if (bitsLeft < 0) { + cachedBits += bitsLeft; + bitsLeft = 0; + cache &= (signed int) 0x80000000 >> (cachedBits - 1); + } + x += (int) (cache >> (32 - linBits)); + cachedBits -= linBits; + cache <<= linBits; + } + if (x) { + ApplySign(x, cache); + cache <<= 1; + cachedBits--; + } + + if (y == 15 && tabType == loopLinbits) { + minBits = linBits + 1; + if (cachedBits + bitsLeft < minBits) + return -1; + while (cachedBits < minBits) { + cache |= (unsigned int) (*buf++) << (24 - cachedBits); + cachedBits += 8; + bitsLeft -= 8; + } + if (bitsLeft < 0) { + cachedBits += bitsLeft; + bitsLeft = 0; + cache &= (signed int) 0x80000000 >> (cachedBits - 1); + } + y += (int) (cache >> (32 - linBits)); + cachedBits -= linBits; + cache <<= linBits; + } + if (y) { + ApplySign(y, cache); + cache <<= 1; + cachedBits--; + } + + /* ran out of bits - should never have consumed padBits */ + if (cachedBits < padBits) + return -1; + + *xy++ = x; + *xy++ = y; + nVals -= 2; + tCurr = tBase; + } + } + bitsLeft += (cachedBits - padBits); + return (startBits - bitsLeft); + } + + /* error in bitstream - trying to access unused Huffman table */ + return -1; +} + +/************************************************************************************** + * Function: DecodeHuffmanQuads + * + * Description: decode 4-way vector Huffman codes in the "count1" region of + *spectrum + * + * Inputs: valid BitStreamInfo struct, pointing to start of quadword codes + * pointer to vwxy buffer to received decoded values + * maximum number of codewords to decode + * index of quadword table (0 = table A, 1 = table B) + * number of bits remaining in bitstream + * + * Outputs: quadruples of decoded coefficients in vwxy + * updated BitStreamInfo struct + * + * Return: index of the first "zero_part" value (index of the first sample + * of the quad word after which all samples are 0) + * + * Notes: si_huff.bit tests every vwxy output in both quad tables + **************************************************************************************/ +// no improvement with section=data +static int DecodeHuffmanQuads(int *vwxy, int nVals, int tabIdx, int bitsLeft, unsigned char *buf, int bitOffset) { + int i, v, w, x, y; + int len, maxBits, cachedBits, padBits; + unsigned int cache; + unsigned char cw, *tBase; + + if (bitsLeft <= 0) + return 0; + + tBase = (unsigned char *) quadTable + quadTabOffset[tabIdx]; + maxBits = quadTabMaxBits[tabIdx]; + + /* initially fill cache with any partial byte */ + cache = 0; + cachedBits = (8 - bitOffset) & 0x07; + if (cachedBits) + cache = (unsigned int) (*buf++) << (32 - cachedBits); + bitsLeft -= cachedBits; + + i = padBits = 0; + while (i < (nVals - 3)) { + /* refill cache - assumes cachedBits <= 16 */ + if (bitsLeft >= 16) { + /* load 2 new bytes into left-justified cache */ + cache |= (unsigned int) (*buf++) << (24 - cachedBits); + cache |= (unsigned int) (*buf++) << (16 - cachedBits); + cachedBits += 16; + bitsLeft -= 16; + } else { + /* last time through, pad cache with zeros and drain cache */ + if (cachedBits + bitsLeft <= 0) + return i; + if (bitsLeft > 0) + cache |= (unsigned int) (*buf++) << (24 - cachedBits); + if (bitsLeft > 8) + cache |= (unsigned int) (*buf++) << (16 - cachedBits); + cachedBits += bitsLeft; + bitsLeft = 0; + + cache &= (signed int) 0x80000000 >> (cachedBits - 1); + padBits = 10; + cachedBits += padBits; /* okay if this is > 32 (0's automatically shifted + in from right) */ + } + + /* largest maxBits = 6, plus 4 for sign bits, so make sure cache has at + * least 10 bits */ + while (i < (nVals - 3) && cachedBits >= 10) { + cw = tBase[cache >> (32 - maxBits)]; + len = GetHLenQ(cw); + cachedBits -= len; + cache <<= len; + + v = GetCWVQ(cw); + if (v) { + ApplySign(v, cache); + cache <<= 1; + cachedBits--; + } + w = GetCWWQ(cw); + if (w) { + ApplySign(w, cache); + cache <<= 1; + cachedBits--; + } + x = GetCWXQ(cw); + if (x) { + ApplySign(x, cache); + cache <<= 1; + cachedBits--; + } + y = GetCWYQ(cw); + if (y) { + ApplySign(y, cache); + cache <<= 1; + cachedBits--; + } + + /* ran out of bits - okay (means we're done) */ + if (cachedBits < padBits) + return i; + + *vwxy++ = v; + *vwxy++ = w; + *vwxy++ = x; + *vwxy++ = y; + i += 4; + } + } + + /* decoded max number of quad values */ + return i; +} + +/************************************************************************************** + * Function: DecodeHuffman + * + * Description: decode one granule, one channel worth of Huffman codes + * + * Inputs: MP3DecInfo structure filled by UnpackFrameHeader(), + *UnpackSideInfo(), and UnpackScaleFactors() (for this granule) buffer pointing + *to start of Huffman data in MP3 frame pointer to bit offset (0-7) indicating + *starting bit in buf[0] number of bits in the Huffman data section of the frame + * (could include padding bits) + * index of current granule and channel + * + * Outputs: decoded coefficients in hi->huffDecBuf[ch] (hi pointer in + *mp3DecInfo) updated bitOffset + * + * Return: length (in bytes) of Huffman codes + * bitOffset also returned in parameter (0 = MSB, 7 = LSB of + * byte located at buf + offset) + * -1 if null input pointers, huffBlockBits < 0, or decoder runs + * out of bits prematurely (invalid bitstream) + **************************************************************************************/ +// .data about 1ms faster per frame +int DecodeHuffman(MP3DecInfo *mp3DecInfo, unsigned char *buf, int *bitOffset, int huffBlockBits, int gr, int ch) { + int r1Start, r2Start, rEnd[4]; /* region boundaries */ + int i, w, bitsUsed, bitsLeft; + unsigned char *startBuf = buf; + + FrameHeader *fh; + SideInfo *si; + SideInfoSub *sis; + ScaleFactorInfo *sfi; + HuffmanInfo *hi; + + /* validate pointers */ + if (!mp3DecInfo || !mp3DecInfo->FrameHeaderPS || !mp3DecInfo->SideInfoPS || !mp3DecInfo->ScaleFactorInfoPS || + !mp3DecInfo->HuffmanInfoPS) + return -1; + + fh = ((FrameHeader *) (mp3DecInfo->FrameHeaderPS)); + si = ((SideInfo *) (mp3DecInfo->SideInfoPS)); + sis = &si->sis[gr][ch]; + sfi = ((ScaleFactorInfo *) (mp3DecInfo->ScaleFactorInfoPS)); + hi = (HuffmanInfo *) (mp3DecInfo->HuffmanInfoPS); + + if (huffBlockBits < 0) + return -1; + + /* figure out region boundaries (the first 2*bigVals coefficients divided into + * 3 regions) */ + if (sis->winSwitchFlag && sis->blockType == 2) { + if (sis->mixedBlock == 0) { + r1Start = fh->sfBand->s[(sis->region0Count + 1) / 3] * 3; + } else { + if (fh->ver == MPEG1) { + r1Start = fh->sfBand->l[sis->region0Count + 1]; + } else { + /* see MPEG2 spec for explanation */ + w = fh->sfBand->s[4] - fh->sfBand->s[3]; + r1Start = fh->sfBand->l[6] + 2 * w; + } + } + r2Start = MAX_NSAMP; /* short blocks don't have region 2 */ + } else { + r1Start = fh->sfBand->l[sis->region0Count + 1]; + r2Start = fh->sfBand->l[sis->region0Count + 1 + sis->region1Count + 1]; + } + + /* offset rEnd index by 1 so first region = rEnd[1] - rEnd[0], etc. */ + rEnd[3] = MIN(MAX_NSAMP, 2 * sis->nBigvals); + rEnd[2] = MIN(r2Start, rEnd[3]); + rEnd[1] = MIN(r1Start, rEnd[3]); + rEnd[0] = 0; + + /* rounds up to first all-zero pair (we don't check last pair for (x,y) == + * (non-zero, zero)) */ + hi->nonZeroBound[ch] = rEnd[3]; + + /* decode Huffman pairs (rEnd[i] are always even numbers) */ + bitsLeft = huffBlockBits; + for (i = 0; i < 3; i++) { + bitsUsed = DecodeHuffmanPairs(hi->huffDecBuf[ch] + rEnd[i], rEnd[i + 1] - rEnd[i], sis->tableSelect[i], bitsLeft, + buf, *bitOffset); + if (bitsUsed < 0 || bitsUsed > bitsLeft) /* error - overran end of bitstream */ + return -1; + + /* update bitstream position */ + buf += (bitsUsed + *bitOffset) >> 3; + *bitOffset = (bitsUsed + *bitOffset) & 0x07; + bitsLeft -= bitsUsed; + } + + /* decode Huffman quads (if any) */ + hi->nonZeroBound[ch] += DecodeHuffmanQuads(hi->huffDecBuf[ch] + rEnd[3], MAX_NSAMP - rEnd[3], sis->count1TableSelect, + bitsLeft, buf, *bitOffset); + + ASSERT(hi->nonZeroBound[ch] <= MAX_NSAMP); + for (i = hi->nonZeroBound[ch]; i < MAX_NSAMP; i++) + hi->huffDecBuf[ch][i] = 0; + + /* If bits used for 576 samples < huffBlockBits, then the extras are + * considered to be stuffing bits (throw away, but need to return correct + * bitstream position) + */ + buf += (bitsLeft + *bitOffset) >> 3; + *bitOffset = (bitsLeft + *bitOffset) & 0x07; + + return (buf - startBuf); +} + +/************************************************************************************** + * Function: Dequantize + * + * Description: dequantize coefficients, decode stereo, reorder short blocks + * (one granule-worth) + * + * Inputs: MP3DecInfo structure filled by UnpackFrameHeader(), + *UnpackSideInfo(), UnpackScaleFactors(), and DecodeHuffman() (for this granule) + * index of current granule + * + * Outputs: dequantized and reordered coefficients in hi->huffDecBuf + * (one granule-worth, all channels), format = Q26 + * operates in-place on huffDecBuf but also needs di->workBuf + * updated hi->nonZeroBound index for both channels + * + * Return: 0 on success, -1 if null input pointers + * + * Notes: In calling output Q(DQ_FRACBITS_OUT), we assume an implicit bias + * of 2^15. Some (floating-point) reference implementations + *factor this into the 2^(0.25 * gain) scaling explicitly. But to avoid + *precision loss, we don't do that. Instead take it into account in the final + * round to PCM (>> by 15 less than we otherwise would have). + * Equivalently, we can think of the dequantized coefficients as + * Q(DQ_FRACBITS_OUT - 15) with no implicit bias. + **************************************************************************************/ +int Dequantize(MP3DecInfo *mp3DecInfo, int gr) { + int i, ch, nSamps, mOut[2]; + FrameHeader *fh; + SideInfo *si; + ScaleFactorInfo *sfi; + HuffmanInfo *hi; + DequantInfo *di; + CriticalBandInfo *cbi; + + /* validate pointers */ + if (!mp3DecInfo || !mp3DecInfo->FrameHeaderPS || !mp3DecInfo->SideInfoPS || !mp3DecInfo->ScaleFactorInfoPS || + !mp3DecInfo->HuffmanInfoPS || !mp3DecInfo->DequantInfoPS) + return -1; + + fh = (FrameHeader *) (mp3DecInfo->FrameHeaderPS); + + /* si is an array of up to 4 structs, stored as gr0ch0, gr0ch1, gr1ch0, gr1ch1 + */ + si = (SideInfo *) (mp3DecInfo->SideInfoPS); + sfi = (ScaleFactorInfo *) (mp3DecInfo->ScaleFactorInfoPS); + hi = (HuffmanInfo *) mp3DecInfo->HuffmanInfoPS; + di = (DequantInfo *) mp3DecInfo->DequantInfoPS; + cbi = di->cbi; + mOut[0] = mOut[1] = 0; + + /* dequantize all the samples in each channel */ + for (ch = 0; ch < mp3DecInfo->nChans; ch++) { + hi->gb[ch] = DequantChannel(hi->huffDecBuf[ch], di->workBuf, &hi->nonZeroBound[ch], fh, &si->sis[gr][ch], + &sfi->sfis[gr][ch], &cbi[ch]); + } + + /* joint stereo processing assumes one guard bit in input samples + * it's extremely rare not to have at least one gb, so if this is the case + * just make a pass over the data and clip to [-2^30+1, 2^30-1] + * in practice this may never happen + */ + if (fh->modeExt && (hi->gb[0] < 1 || hi->gb[1] < 1)) { + for (i = 0; i < hi->nonZeroBound[0]; i++) { + if (hi->huffDecBuf[0][i] < -0x3fffffff) + hi->huffDecBuf[0][i] = -0x3fffffff; + if (hi->huffDecBuf[0][i] > 0x3fffffff) + hi->huffDecBuf[0][i] = 0x3fffffff; + } + for (i = 0; i < hi->nonZeroBound[1]; i++) { + if (hi->huffDecBuf[1][i] < -0x3fffffff) + hi->huffDecBuf[1][i] = -0x3fffffff; + if (hi->huffDecBuf[1][i] > 0x3fffffff) + hi->huffDecBuf[1][i] = 0x3fffffff; + } + } + + /* do mid-side stereo processing, if enabled */ + if (fh->modeExt >> 1) { + if (fh->modeExt & 0x01) { + /* intensity stereo enabled - run mid-side up to start of right zero + * region */ + if (cbi[1].cbType == 0) + nSamps = fh->sfBand->l[cbi[1].cbEndL + 1]; + else + nSamps = 3 * fh->sfBand->s[cbi[1].cbEndSMax + 1]; + } else { + /* intensity stereo disabled - run mid-side on whole spectrum */ + nSamps = MAX(hi->nonZeroBound[0], hi->nonZeroBound[1]); + } + MidSideProc(hi->huffDecBuf, nSamps, mOut); + } + + /* do intensity stereo processing, if enabled */ + if (fh->modeExt & 0x01) { + nSamps = hi->nonZeroBound[0]; + if (fh->ver == MPEG1) { + IntensityProcMPEG1(hi->huffDecBuf, nSamps, fh, &sfi->sfis[gr][1], di->cbi, fh->modeExt >> 1, + si->sis[gr][1].mixedBlock, mOut); + } else { + IntensityProcMPEG2(hi->huffDecBuf, nSamps, fh, &sfi->sfis[gr][1], di->cbi, &sfi->sfjs, fh->modeExt >> 1, + si->sis[gr][1].mixedBlock, mOut); + } + } + + /* adjust guard bit count and nonZeroBound if we did any stereo processing */ + if (fh->modeExt) { + hi->gb[0] = CLZ(mOut[0]) - 1; + hi->gb[1] = CLZ(mOut[1]) - 1; + nSamps = MAX(hi->nonZeroBound[0], hi->nonZeroBound[1]); + hi->nonZeroBound[0] = nSamps; + hi->nonZeroBound[1] = nSamps; + } + + /* output format Q(DQ_FRACBITS_OUT) */ + return 0; +} + +#define COS0_0 0x4013c251 /* Q31 */ +#define COS0_1 0x40b345bd /* Q31 */ +#define COS0_2 0x41fa2d6d /* Q31 */ +#define COS0_3 0x43f93421 /* Q31 */ +#define COS0_4 0x46cc1bc4 /* Q31 */ +#define COS0_5 0x4a9d9cf0 /* Q31 */ +#define COS0_6 0x4fae3711 /* Q31 */ +#define COS0_7 0x56601ea7 /* Q31 */ +#define COS0_8 0x5f4cf6eb /* Q31 */ +#define COS0_9 0x6b6fcf26 /* Q31 */ +#define COS0_10 0x7c7d1db3 /* Q31 */ +#define COS0_11 0x4ad81a97 /* Q30 */ +#define COS0_12 0x5efc8d96 /* Q30 */ +#define COS0_13 0x41d95790 /* Q29 */ +#define COS0_14 0x6d0b20cf /* Q29 */ +#define COS0_15 0x518522fb /* Q27 */ + +#define COS1_0 0x404f4672 /* Q31 */ +#define COS1_1 0x42e13c10 /* Q31 */ +#define COS1_2 0x48919f44 /* Q31 */ +#define COS1_3 0x52cb0e63 /* Q31 */ +#define COS1_4 0x64e2402e /* Q31 */ +#define COS1_5 0x43e224a9 /* Q30 */ +#define COS1_6 0x6e3c92c1 /* Q30 */ +#define COS1_7 0x519e4e04 /* Q28 */ + +#define COS2_0 0x4140fb46 /* Q31 */ +#define COS2_1 0x4cf8de88 /* Q31 */ +#define COS2_2 0x73326bbf /* Q31 */ +#define COS2_3 0x52036742 /* Q29 */ + +#define COS3_0 0x4545e9ef /* Q31 */ +#define COS3_1 0x539eba45 /* Q30 */ + +#define COS4_0 0x5a82799a /* Q31 */ + +// faster in ROM +static const int dcttab[48] = { + /* first pass */ + COS0_0, COS0_15, COS1_0, /* 31, 27, 31 */ + COS0_1, COS0_14, COS1_1, /* 31, 29, 31 */ + COS0_2, COS0_13, COS1_2, /* 31, 29, 31 */ + COS0_3, COS0_12, COS1_3, /* 31, 30, 31 */ + COS0_4, COS0_11, COS1_4, /* 31, 30, 31 */ + COS0_5, COS0_10, COS1_5, /* 31, 31, 30 */ + COS0_6, COS0_9, COS1_6, /* 31, 31, 30 */ + COS0_7, COS0_8, COS1_7, /* 31, 31, 28 */ + /* second pass */ + COS2_0, COS2_3, COS3_0, /* 31, 29, 31 */ + COS2_1, COS2_2, COS3_1, /* 31, 31, 30 */ + -COS2_0, -COS2_3, COS3_0, /* 31, 29, 31 */ + -COS2_1, -COS2_2, COS3_1, /* 31, 31, 30 */ + COS2_0, COS2_3, COS3_0, /* 31, 29, 31 */ + COS2_1, COS2_2, COS3_1, /* 31, 31, 30 */ + -COS2_0, -COS2_3, COS3_0, /* 31, 29, 31 */ + -COS2_1, -COS2_2, COS3_1, /* 31, 31, 30 */ +}; + +#define D32FP(i, s0, s1, s2) \ + { \ + a0 = buf[i]; \ + a3 = buf[31 - i]; \ + a1 = buf[15 - i]; \ + a2 = buf[16 + i]; \ + b0 = a0 + a3; \ + b3 = MULSHIFT32(*cptr++, a0 - a3) << (s0); \ + b1 = a1 + a2; \ + b2 = MULSHIFT32(*cptr++, a1 - a2) << (s1); \ + buf[i] = b0 + b1; \ + buf[15 - i] = MULSHIFT32(*cptr, b0 - b1) << (s2); \ + buf[16 + i] = b2 + b3; \ + buf[31 - i] = MULSHIFT32(*cptr++, b3 - b2) << (s2); \ + } + +/************************************************************************************** + * Function: FDCT32 + * + * Description: Ken's highly-optimized 32-point DCT (radix-4 + radix-8) + * + * Inputs: input buffer, length = 32 samples + * require at least 6 guard bits in input vector x to avoid + *possibility of overflow in internal calculations (see bbtest_imdct test app) + * buffer offset and oddblock flag for polyphase filter input + *buffer number of guard bits in input + * + * Outputs: output buffer, data copied and interleaved for polyphase filter + * no guarantees about number of guard bits in output + * + * Return: none + * + * Notes: number of muls = 4*8 + 12*4 = 80 + * final stage of DCT is hardcoded to shuffle data into the proper + *order for the polyphase filterbank fully unrolled stage 1, for max precision + *(scale the 1/cos() factors differently, depending on magnitude) guard bit + *analysis verified by exhaustive testing of all 2^32 combinations of max + *pos/max neg values in x[] + * + * TODO: code organization and optimization for ARM + * possibly interleave stereo (cut # of coef loads in half - may + *not have enough registers) + **************************************************************************************/ +// about 1ms faster in RAM +void FDCT32(int *buf, int *dest, int offset, int oddBlock, int gb) { + int i, s, tmp, es; + const int *cptr = dcttab; + int a0, a1, a2, a3, a4, a5, a6, a7; + int b0, b1, b2, b3, b4, b5, b6, b7; + int *d; + + /* scaling - ensure at least 6 guard bits for DCT + * (in practice this is already true 99% of time, so this code is + * almost never triggered) + */ + es = 0; + if (gb < 6) { + es = 6 - gb; + for (i = 0; i < 32; i++) + buf[i] >>= es; + } + + /* first pass */ + D32FP(0, 1, 5, 1); + D32FP(1, 1, 3, 1); + D32FP(2, 1, 3, 1); + D32FP(3, 1, 2, 1); + D32FP(4, 1, 2, 1); + D32FP(5, 1, 1, 2); + D32FP(6, 1, 1, 2); + D32FP(7, 1, 1, 4); + + /* second pass */ + for (i = 4; i > 0; i--) { + a0 = buf[0]; + a7 = buf[7]; + a3 = buf[3]; + a4 = buf[4]; + b0 = a0 + a7; + b7 = MULSHIFT32(*cptr++, a0 - a7) << 1; + b3 = a3 + a4; + b4 = MULSHIFT32(*cptr++, a3 - a4) << 3; + a0 = b0 + b3; + a3 = MULSHIFT32(*cptr, b0 - b3) << 1; + a4 = b4 + b7; + a7 = MULSHIFT32(*cptr++, b7 - b4) << 1; + + a1 = buf[1]; + a6 = buf[6]; + a2 = buf[2]; + a5 = buf[5]; + b1 = a1 + a6; + b6 = MULSHIFT32(*cptr++, a1 - a6) << 1; + b2 = a2 + a5; + b5 = MULSHIFT32(*cptr++, a2 - a5) << 1; + a1 = b1 + b2; + a2 = MULSHIFT32(*cptr, b1 - b2) << 2; + a5 = b5 + b6; + a6 = MULSHIFT32(*cptr++, b6 - b5) << 2; + + b0 = a0 + a1; + b1 = MULSHIFT32(COS4_0, a0 - a1) << 1; + b2 = a2 + a3; + b3 = MULSHIFT32(COS4_0, a3 - a2) << 1; + buf[0] = b0; + buf[1] = b1; + buf[2] = b2 + b3; + buf[3] = b3; + + b4 = a4 + a5; + b5 = MULSHIFT32(COS4_0, a4 - a5) << 1; + b6 = a6 + a7; + b7 = MULSHIFT32(COS4_0, a7 - a6) << 1; + b6 += b7; + buf[4] = b4 + b6; + buf[5] = b5 + b7; + buf[6] = b5 + b6; + buf[7] = b7; + + buf += 8; + } + buf -= 32; /* reset */ + + /* sample 0 - always delayed one block */ + d = dest + 64 * 16 + ((offset - oddBlock) & 7) + (oddBlock ? 0 : VBUF_LENGTH); + s = buf[0]; + d[0] = d[8] = s; + + /* samples 16 to 31 */ + d = dest + offset + (oddBlock ? VBUF_LENGTH : 0); + + s = buf[1]; + d[0] = d[8] = s; + d += 64; + + tmp = buf[25] + buf[29]; + s = buf[17] + tmp; + d[0] = d[8] = s; + d += 64; + s = buf[9] + buf[13]; + d[0] = d[8] = s; + d += 64; + s = buf[21] + tmp; + d[0] = d[8] = s; + d += 64; + + tmp = buf[29] + buf[27]; + s = buf[5]; + d[0] = d[8] = s; + d += 64; + s = buf[21] + tmp; + d[0] = d[8] = s; + d += 64; + s = buf[13] + buf[11]; + d[0] = d[8] = s; + d += 64; + s = buf[19] + tmp; + d[0] = d[8] = s; + d += 64; + + tmp = buf[27] + buf[31]; + s = buf[3]; + d[0] = d[8] = s; + d += 64; + s = buf[19] + tmp; + d[0] = d[8] = s; + d += 64; + s = buf[11] + buf[15]; + d[0] = d[8] = s; + d += 64; + s = buf[23] + tmp; + d[0] = d[8] = s; + d += 64; + + tmp = buf[31]; + s = buf[7]; + d[0] = d[8] = s; + d += 64; + s = buf[23] + tmp; + d[0] = d[8] = s; + d += 64; + s = buf[15]; + d[0] = d[8] = s; + d += 64; + s = tmp; + d[0] = d[8] = s; + + /* samples 16 to 1 (sample 16 used again) */ + d = dest + 16 + ((offset - oddBlock) & 7) + (oddBlock ? 0 : VBUF_LENGTH); + + s = buf[1]; + d[0] = d[8] = s; + d += 64; + + tmp = buf[30] + buf[25]; + s = buf[17] + tmp; + d[0] = d[8] = s; + d += 64; + s = buf[14] + buf[9]; + d[0] = d[8] = s; + d += 64; + s = buf[22] + tmp; + d[0] = d[8] = s; + d += 64; + s = buf[6]; + d[0] = d[8] = s; + d += 64; + + tmp = buf[26] + buf[30]; + s = buf[22] + tmp; + d[0] = d[8] = s; + d += 64; + s = buf[10] + buf[14]; + d[0] = d[8] = s; + d += 64; + s = buf[18] + tmp; + d[0] = d[8] = s; + d += 64; + s = buf[2]; + d[0] = d[8] = s; + d += 64; + + tmp = buf[28] + buf[26]; + s = buf[18] + tmp; + d[0] = d[8] = s; + d += 64; + s = buf[12] + buf[10]; + d[0] = d[8] = s; + d += 64; + s = buf[20] + tmp; + d[0] = d[8] = s; + d += 64; + s = buf[4]; + d[0] = d[8] = s; + d += 64; + + tmp = buf[24] + buf[28]; + s = buf[20] + tmp; + d[0] = d[8] = s; + d += 64; + s = buf[8] + buf[12]; + d[0] = d[8] = s; + d += 64; + s = buf[16] + tmp; + d[0] = d[8] = s; + + /* this is so rarely invoked that it's not worth making two versions of the + * output shuffle code (one for no shift, one for clip + variable shift) like + * in IMDCT here we just load, clip, shift, and store on the rare instances + * that es != 0 + */ + if (es) { + d = dest + 64 * 16 + ((offset - oddBlock) & 7) + (oddBlock ? 0 : VBUF_LENGTH); + s = d[0]; + CLIP_2N(s, 31 - es); + d[0] = d[8] = (s << es); + + d = dest + offset + (oddBlock ? VBUF_LENGTH : 0); + for (i = 16; i <= 31; i++) { + s = d[0]; + CLIP_2N(s, 31 - es); + d[0] = d[8] = (s << es); + d += 64; + } + + d = dest + 16 + ((offset - oddBlock) & 7) + (oddBlock ? 0 : VBUF_LENGTH); + for (i = 15; i >= 0; i--) { + s = d[0]; + CLIP_2N(s, 31 - es); + d[0] = d[8] = (s << es); + d += 64; + } + } +} + +/************************************************************************************** + * Function: ClearBuffer + * + * Description: fill buffer with 0's + * + * Inputs: pointer to buffer + * number of bytes to fill with 0 + * + * Outputs: cleared buffer + * + * Return: none + * + * Notes: slow, platform-independent equivalent to memset(buf, 0, nBytes) + **************************************************************************************/ +static void ClearBuffer(void *buf, int nBytes) { + int i; + unsigned char *cbuf = (unsigned char *) buf; + + for (i = 0; i < nBytes; i++) + cbuf[i] = 0; + + return; +} + +/************************************************************************************** + * Function: AllocateBuffers + * + * Description: allocate all the memory needed for the MP3 decoder + * + * Inputs: none + * + * Outputs: none + * + * Return: pointer to MP3DecInfo structure (initialized with pointers to + *all the internal buffers needed for decoding, all other members of MP3DecInfo + *structure set to 0) + * + * Notes: if one or more mallocs fail, function frees any buffers already + * allocated before returning + **************************************************************************************/ +MP3DecInfo *AllocateBuffers(void) { + MP3DecInfo *mp3DecInfo; + FrameHeader *fh; + SideInfo *si; + ScaleFactorInfo *sfi; + HuffmanInfo *hi; + DequantInfo *di; + IMDCTInfo *mi; + SubbandInfo *sbi; + + esphome::ExternalRAMAllocator mp3di_allocator(esphome::ExternalRAMAllocator::ALLOW_FAILURE); + esphome::ExternalRAMAllocator fh_allocator(esphome::ExternalRAMAllocator::ALLOW_FAILURE); + esphome::ExternalRAMAllocator si_allocator(esphome::ExternalRAMAllocator::ALLOW_FAILURE); + esphome::ExternalRAMAllocator sfi_allocator( + esphome::ExternalRAMAllocator::ALLOW_FAILURE); + esphome::ExternalRAMAllocator hi_allocator(esphome::ExternalRAMAllocator::ALLOW_FAILURE); + esphome::ExternalRAMAllocator di_allocator(esphome::ExternalRAMAllocator::ALLOW_FAILURE); + esphome::ExternalRAMAllocator mi_allocator(esphome::ExternalRAMAllocator::ALLOW_FAILURE); + esphome::ExternalRAMAllocator sbi_allocator(esphome::ExternalRAMAllocator::ALLOW_FAILURE); + + // mp3DecInfo = (MP3DecInfo *)malloc(sizeof(MP3DecInfo)); + mp3DecInfo = mp3di_allocator.allocate(1); + if (!mp3DecInfo) + return 0; + ClearBuffer(mp3DecInfo, sizeof(MP3DecInfo)); + + // fh = (FrameHeader *)malloc(sizeof(FrameHeader)); + // si = (SideInfo *)malloc(sizeof(SideInfo)); + // sfi = (ScaleFactorInfo *)malloc(sizeof(ScaleFactorInfo)); + // hi = (HuffmanInfo *)malloc(sizeof(HuffmanInfo)); + // di = (DequantInfo *)malloc(sizeof(DequantInfo)); + // mi = (IMDCTInfo *)malloc(sizeof(IMDCTInfo)); + // sbi = (SubbandInfo *)malloc(sizeof(SubbandInfo)); + fh = fh_allocator.allocate(1); + si = si_allocator.allocate(1); + sfi = sfi_allocator.allocate(1); + hi = hi_allocator.allocate(1); + di = di_allocator.allocate(1); + mi = mi_allocator.allocate(1); + sbi = sbi_allocator.allocate(1); + + mp3DecInfo->FrameHeaderPS = (void *) fh; + mp3DecInfo->SideInfoPS = (void *) si; + mp3DecInfo->ScaleFactorInfoPS = (void *) sfi; + mp3DecInfo->HuffmanInfoPS = (void *) hi; + mp3DecInfo->DequantInfoPS = (void *) di; + mp3DecInfo->IMDCTInfoPS = (void *) mi; + mp3DecInfo->SubbandInfoPS = (void *) sbi; + + if (!fh || !si || !sfi || !hi || !di || !mi || !sbi) { + FreeBuffers(mp3DecInfo); /* safe to call - only frees memory that was + successfully allocated */ + return 0; + } + + /* important to do this - DSP primitives assume a bunch of state variables are + * 0 on first use */ + ClearBuffer(fh, sizeof(FrameHeader)); + ClearBuffer(si, sizeof(SideInfo)); + ClearBuffer(sfi, sizeof(ScaleFactorInfo)); + ClearBuffer(hi, sizeof(HuffmanInfo)); + ClearBuffer(di, sizeof(DequantInfo)); + ClearBuffer(mi, sizeof(IMDCTInfo)); + ClearBuffer(sbi, sizeof(SubbandInfo)); + + return mp3DecInfo; +} + +#define SAFE_FREE(x) \ + { \ + if (x) \ + free(x); \ + (x) = 0; \ + } /* helper macro */ + +/************************************************************************************** + * Function: FreeBuffers + * + * Description: frees all the memory used by the MP3 decoder + * + * Inputs: pointer to initialized MP3DecInfo structure + * + * Outputs: none + * + * Return: none + * + * Notes: safe to call even if some buffers were not allocated (uses + *SAFE_FREE) + **************************************************************************************/ +void FreeBuffers(MP3DecInfo *mp3DecInfo) { + if (!mp3DecInfo) + return; + + SAFE_FREE(mp3DecInfo->FrameHeaderPS); + SAFE_FREE(mp3DecInfo->SideInfoPS); + SAFE_FREE(mp3DecInfo->ScaleFactorInfoPS); + SAFE_FREE(mp3DecInfo->HuffmanInfoPS); + SAFE_FREE(mp3DecInfo->DequantInfoPS); + SAFE_FREE(mp3DecInfo->IMDCTInfoPS); + SAFE_FREE(mp3DecInfo->SubbandInfoPS); + + SAFE_FREE(mp3DecInfo); +} + +/************************************************************************************** + * Function: SetBitstreamPointer + * + * Description: initialize bitstream reader + * + * Inputs: pointer to BitStreamInfo struct + * number of bytes in bitstream + * pointer to byte-aligned buffer of data to read from + * + * Outputs: filled bitstream info struct + * + * Return: none + **************************************************************************************/ +void SetBitstreamPointer(BitStreamInfo *bsi, int nBytes, unsigned char *buf) { + /* init bitstream */ + bsi->bytePtr = buf; + bsi->iCache = 0; /* 4-byte unsigned int */ + bsi->cachedBits = 0; /* i.e. zero bits in cache */ + bsi->nBytes = nBytes; +} + +/************************************************************************************** + * Function: RefillBitstreamCache + * + * Description: read new data from bitstream buffer into bsi cache + * + * Inputs: pointer to initialized BitStreamInfo struct + * + * Outputs: updated bitstream info struct + * + * Return: none + * + * Notes: only call when iCache is completely drained (resets bitOffset to + *0) always loads 4 new bytes except when bsi->nBytes < 4 (end of buffer) stores + *data as big-endian in cache, regardless of machine endian-ness + * + * TODO: optimize for ARM + * possibly add little/big-endian modes for doing 32-bit loads + **************************************************************************************/ +static __inline void RefillBitstreamCache(BitStreamInfo *bsi) { + int nBytes = bsi->nBytes; + + /* optimize for common case, independent of machine endian-ness */ + if (nBytes >= 4) { + bsi->iCache = (*bsi->bytePtr++) << 24; + bsi->iCache |= (*bsi->bytePtr++) << 16; + bsi->iCache |= (*bsi->bytePtr++) << 8; + bsi->iCache |= (*bsi->bytePtr++); + bsi->cachedBits = 32; + bsi->nBytes -= 4; + } else { + bsi->iCache = 0; + while (nBytes--) { + bsi->iCache |= (*bsi->bytePtr++); + bsi->iCache <<= 8; + } + bsi->iCache <<= ((3 - bsi->nBytes) * 8); + bsi->cachedBits = 8 * bsi->nBytes; + bsi->nBytes = 0; + } +} + +/************************************************************************************** + * Function: GetBits + * + * Description: get bits from bitstream, advance bitstream pointer + * + * Inputs: pointer to initialized BitStreamInfo struct + * number of bits to get from bitstream + * + * Outputs: updated bitstream info struct + * + * Return: the next nBits bits of data from bitstream buffer + * + * Notes: nBits must be in range [0, 31], nBits outside this range masked + *by 0x1f for speed, does not indicate error if you overrun bit buffer if nBits + *= 0, returns 0 (useful for scalefactor unpacking) + * + * TODO: optimize for ARM + **************************************************************************************/ +unsigned int GetBits(BitStreamInfo *bsi, int nBits) { + unsigned int data, lowBits; + + nBits &= 0x1f; /* nBits mod 32 to avoid unpredictable results like >> by + negative amount */ + data = bsi->iCache >> (31 - nBits); /* unsigned >> so zero-extend */ + data >>= 1; /* do as >> 31, >> 1 so that nBits = 0 works okay (returns 0) */ + bsi->iCache <<= nBits; /* left-justify cache */ + bsi->cachedBits -= nBits; /* how many bits have we drawn from the cache so far */ + + /* if we cross an int boundary, refill the cache */ + if (bsi->cachedBits < 0) { + lowBits = -bsi->cachedBits; + RefillBitstreamCache(bsi); + data |= bsi->iCache >> (32 - lowBits); /* get the low-order bits */ + + bsi->cachedBits -= lowBits; /* how many bits have we drawn from the cache so far */ + bsi->iCache <<= lowBits; /* left-justify cache */ + } + + return data; +} + +/************************************************************************************** + * Function: CalcBitsUsed + * + * Description: calculate how many bits have been read from bitstream + * + * Inputs: pointer to initialized BitStreamInfo struct + * pointer to start of bitstream buffer + * bit offset into first byte of startBuf (0-7) + * + * Outputs: none + * + * Return: number of bits read from bitstream, as offset from + *startBuf:startOffset + **************************************************************************************/ +int CalcBitsUsed(BitStreamInfo *bsi, unsigned char *startBuf, int startOffset) { + int bitsUsed; + + bitsUsed = (bsi->bytePtr - startBuf) * 8; + bitsUsed -= bsi->cachedBits; + bitsUsed -= startOffset; + + return bitsUsed; +} + +/************************************************************************************** + * Function: CheckPadBit + * + * Description: check whether padding byte is present in an MP3 frame + * + * Inputs: MP3DecInfo struct with valid FrameHeader struct + * (filled by UnpackFrameHeader()) + * + * Outputs: none + * + * Return: 1 if pad bit is set, 0 if not, -1 if null input pointer + **************************************************************************************/ +int CheckPadBit(MP3DecInfo *mp3DecInfo) { + FrameHeader *fh; + + /* validate pointers */ + if (!mp3DecInfo || !mp3DecInfo->FrameHeaderPS) + return -1; + + fh = ((FrameHeader *) (mp3DecInfo->FrameHeaderPS)); + + return (fh->paddingBit ? 1 : 0); +} + +/************************************************************************************** + * Function: UnpackFrameHeader + * + * Description: parse the fields of the MP3 frame header + * + * Inputs: buffer pointing to a complete MP3 frame header (4 bytes, plus 2 + *if CRC) + * + * Outputs: filled frame header info in the MP3DecInfo structure + * updated platform-specific FrameHeader struct + * + * Return: length (in bytes) of frame header (for caller to calculate + *offset to first byte following frame header) -1 if null frameHeader or invalid + *header + * + * TODO: check for valid modes, depending on capabilities of decoder + * test CRC on actual stream (verify no endian problems) + **************************************************************************************/ +int UnpackFrameHeader(MP3DecInfo *mp3DecInfo, unsigned char *buf) { + int verIdx; + FrameHeader *fh; + + /* validate pointers and sync word */ + if (!mp3DecInfo || !mp3DecInfo->FrameHeaderPS || (buf[0] & SYNCWORDH) != SYNCWORDH || + (buf[1] & SYNCWORDL) != SYNCWORDL) + return -1; + + fh = ((FrameHeader *) (mp3DecInfo->FrameHeaderPS)); + + /* read header fields - use bitmasks instead of GetBits() for speed, since + * format never varies */ + verIdx = (buf[1] >> 3) & 0x03; + fh->ver = (MPEGVersion) (verIdx == 0 ? MPEG25 : ((verIdx & 0x01) ? MPEG1 : MPEG2)); + fh->layer = 4 - ((buf[1] >> 1) & 0x03); /* easy mapping of index to layer number, 4 = error */ + fh->crc = 1 - ((buf[1] >> 0) & 0x01); + fh->brIdx = (buf[2] >> 4) & 0x0f; + fh->srIdx = (buf[2] >> 2) & 0x03; + fh->paddingBit = (buf[2] >> 1) & 0x01; + fh->privateBit = (buf[2] >> 0) & 0x01; + fh->sMode = (StereoMode) ((buf[3] >> 6) & 0x03); /* maps to correct enum (see definition) */ + fh->modeExt = (buf[3] >> 4) & 0x03; + fh->copyFlag = (buf[3] >> 3) & 0x01; + fh->origFlag = (buf[3] >> 2) & 0x01; + fh->emphasis = (buf[3] >> 0) & 0x03; + + /* check parameters to avoid indexing tables with bad values */ + if (fh->srIdx == 3 || fh->layer == 4 || fh->brIdx == 15) + return -1; + + fh->sfBand = &sfBandTable[fh->ver][fh->srIdx]; /* for readability (we reference + sfBandTable many times in decoder) */ + if (fh->sMode != Joint) /* just to be safe (dequant, stproc check fh->modeExt) */ + fh->modeExt = 0; + + /* init user-accessible data */ + mp3DecInfo->nChans = (fh->sMode == Mono ? 1 : 2); + mp3DecInfo->samprate = samplerateTab[fh->ver][fh->srIdx]; + mp3DecInfo->nGrans = (fh->ver == MPEG1 ? NGRANS_MPEG1 : NGRANS_MPEG2); + mp3DecInfo->nGranSamps = ((int) samplesPerFrameTab[fh->ver][fh->layer - 1]) / mp3DecInfo->nGrans; + mp3DecInfo->layer = fh->layer; + mp3DecInfo->version = fh->ver; + + /* get bitrate and nSlots from table, unless brIdx == 0 (free mode) in which + * case caller must figure it out himself question - do we want to overwrite + * mp3DecInfo->bitrate with 0 each time if it's free mode, and copy the + * pre-calculated actual free bitrate into it in mp3dec.c (according to the + * spec, this shouldn't be necessary, since it should be either all frames + * free or none free) + */ + if (fh->brIdx) { + mp3DecInfo->bitrate = ((int) bitrateTab[fh->ver][fh->layer - 1][fh->brIdx]) * 1000; + + /* nSlots = total frame bytes (from table) - sideInfo bytes - header - CRC + * (if present) + pad (if present) */ + mp3DecInfo->nSlots = (int) slotTab[fh->ver][fh->srIdx][fh->brIdx] - + (int) sideBytesTab[fh->ver][(fh->sMode == Mono ? 0 : 1)] - 4 - (fh->crc ? 2 : 0) + + (fh->paddingBit ? 1 : 0); + } + + /* load crc word, if enabled, and return length of frame header (in bytes) */ + if (fh->crc) { + fh->CRCWord = ((int) buf[4] << 8 | (int) buf[5] << 0); + return 6; + } else { + fh->CRCWord = 0; + return 4; + } +} + +/************************************************************************************** + * Function: UnpackSideInfo + * + * Description: parse the fields of the MP3 side info header + * + * Inputs: MP3DecInfo structure filled by UnpackFrameHeader() + * buffer pointing to the MP3 side info data + * + * Outputs: updated mainDataBegin in MP3DecInfo struct + * updated private (platform-specific) SideInfo struct + * + * Return: length (in bytes) of side info data + * -1 if null input pointers + **************************************************************************************/ +int UnpackSideInfo(MP3DecInfo *mp3DecInfo, unsigned char *buf) { + int gr, ch, bd, nBytes; + BitStreamInfo bitStreamInfo, *bsi; + FrameHeader *fh; + SideInfo *si; + SideInfoSub *sis; + + /* validate pointers and sync word */ + if (!mp3DecInfo || !mp3DecInfo->FrameHeaderPS || !mp3DecInfo->SideInfoPS) + return -1; + + fh = ((FrameHeader *) (mp3DecInfo->FrameHeaderPS)); + si = ((SideInfo *) (mp3DecInfo->SideInfoPS)); + + bsi = &bitStreamInfo; + if (fh->ver == MPEG1) { + /* MPEG 1 */ + nBytes = (fh->sMode == Mono ? SIBYTES_MPEG1_MONO : SIBYTES_MPEG1_STEREO); + SetBitstreamPointer(bsi, nBytes, buf); + si->mainDataBegin = GetBits(bsi, 9); + si->privateBits = GetBits(bsi, (fh->sMode == Mono ? 5 : 3)); + + for (ch = 0; ch < mp3DecInfo->nChans; ch++) + for (bd = 0; bd < MAX_SCFBD; bd++) + si->scfsi[ch][bd] = GetBits(bsi, 1); + } else { + /* MPEG 2, MPEG 2.5 */ + nBytes = (fh->sMode == Mono ? SIBYTES_MPEG2_MONO : SIBYTES_MPEG2_STEREO); + SetBitstreamPointer(bsi, nBytes, buf); + si->mainDataBegin = GetBits(bsi, 8); + si->privateBits = GetBits(bsi, (fh->sMode == Mono ? 1 : 2)); + } + + for (gr = 0; gr < mp3DecInfo->nGrans; gr++) { + for (ch = 0; ch < mp3DecInfo->nChans; ch++) { + sis = &si->sis[gr][ch]; /* side info subblock for this granule, channel */ + + sis->part23Length = GetBits(bsi, 12); + sis->nBigvals = GetBits(bsi, 9); + sis->globalGain = GetBits(bsi, 8); + sis->sfCompress = GetBits(bsi, (fh->ver == MPEG1 ? 4 : 9)); + sis->winSwitchFlag = GetBits(bsi, 1); + + if (sis->winSwitchFlag) { + /* this is a start, stop, short, or mixed block */ + sis->blockType = GetBits(bsi, 2); /* 0 = normal, 1 = start, 2 = short, 3 = stop */ + sis->mixedBlock = GetBits(bsi, 1); /* 0 = not mixed, 1 = mixed */ + sis->tableSelect[0] = GetBits(bsi, 5); + sis->tableSelect[1] = GetBits(bsi, 5); + sis->tableSelect[2] = 0; /* unused */ + sis->subBlockGain[0] = GetBits(bsi, 3); + sis->subBlockGain[1] = GetBits(bsi, 3); + sis->subBlockGain[2] = GetBits(bsi, 3); + + /* TODO - check logic */ + if (sis->blockType == 0) { + /* this should not be allowed, according to spec */ + sis->nBigvals = 0; + sis->part23Length = 0; + sis->sfCompress = 0; + } else if (sis->blockType == 2 && sis->mixedBlock == 0) { + /* short block, not mixed */ + sis->region0Count = 8; + } else { + /* start, stop, or short-mixed */ + sis->region0Count = 7; + } + sis->region1Count = 20 - sis->region0Count; + } else { + /* this is a normal block */ + sis->blockType = 0; + sis->mixedBlock = 0; + sis->tableSelect[0] = GetBits(bsi, 5); + sis->tableSelect[1] = GetBits(bsi, 5); + sis->tableSelect[2] = GetBits(bsi, 5); + sis->region0Count = GetBits(bsi, 4); + sis->region1Count = GetBits(bsi, 3); + } + sis->preFlag = (fh->ver == MPEG1 ? GetBits(bsi, 1) : 0); + sis->sfactScale = GetBits(bsi, 1); + sis->count1TableSelect = GetBits(bsi, 1); + } + } + mp3DecInfo->mainDataBegin = si->mainDataBegin; /* needed by main decode loop */ + + ASSERT(nBytes == CalcBitsUsed(bsi, buf, 0) >> 3); + + return nBytes; +} + +/************************************************************************************** + * Function: MP3InitDecoder + * + * Description: allocate memory for platform-specific data + * clear all the user-accessible fields + * + * Inputs: none + * + * Outputs: none + * + * Return: handle to mp3 decoder instance, 0 if malloc fails + **************************************************************************************/ +HMP3Decoder MP3InitDecoder(void) { + MP3DecInfo *mp3DecInfo; + + mp3DecInfo = AllocateBuffers(); + + return (HMP3Decoder) mp3DecInfo; +} + +/************************************************************************************** + * Function: MP3FreeDecoder + * + * Description: free platform-specific data allocated by InitMP3Decoder + * zero out the contents of MP3DecInfo struct + * + * Inputs: valid MP3 decoder instance pointer (HMP3Decoder) + * + * Outputs: none + * + * Return: none + **************************************************************************************/ +void MP3FreeDecoder(HMP3Decoder hMP3Decoder) { + MP3DecInfo *mp3DecInfo = (MP3DecInfo *) hMP3Decoder; + + if (!mp3DecInfo) + return; + + FreeBuffers(mp3DecInfo); +} + +/************************************************************************************** + * Function: MP3FindSyncWord + * + * Description: locate the next byte-alinged sync word in the raw mp3 stream + * + * Inputs: buffer to search for sync word + * max number of bytes to search in buffer + * + * Outputs: none + * + * Return: offset to first sync word (bytes from start of buf) + * -1 if sync not found after searching nBytes + **************************************************************************************/ +int MP3FindSyncWord(unsigned char *buf, int nBytes) { + int i; + + /* find byte-aligned syncword - need 12 (MPEG 1,2) or 11 (MPEG 2.5) matching + * bits */ + for (i = 0; i < nBytes - 1; i++) { + if ((buf[i + 0] & SYNCWORDH) == SYNCWORDH && (buf[i + 1] & SYNCWORDL) == SYNCWORDL) + return i; + } + + return -1; +} + +/************************************************************************************** + * Function: MP3FindFreeSync + * + * Description: figure out number of bytes between adjacent sync words in "free" + *mode + * + * Inputs: buffer to search for next sync word + * the 4-byte frame header starting at the current sync word + * max number of bytes to search in buffer + * + * Outputs: none + * + * Return: offset to next sync word, minus any pad byte (i.e. nSlots) + * -1 if sync not found after searching nBytes + * + * Notes: this checks that the first 22 bits of the next frame header are + *the same as the current frame header, but it's still not foolproof (could + *accidentally find a sequence in the bitstream which appears to match but is + *not actually the next frame header) this could be made more error-resilient by + *checking several frames in a row and verifying that nSlots is the same in each + *case since free mode requires CBR (see spec) we generally only call this + *function once (first frame) then store the result (nSlots) and just use it + *from then on + **************************************************************************************/ +static int MP3FindFreeSync(unsigned char *buf, unsigned char firstFH[4], int nBytes) { + int offset = 0; + unsigned char *bufPtr = buf; + + /* loop until we either: + * - run out of nBytes (FindMP3SyncWord() returns -1) + * - find the next valid frame header (sync word, version, layer, CRC flag, + * bitrate, and sample rate in next header must match current header) + */ + while (1) { + offset = MP3FindSyncWord(bufPtr, nBytes); + bufPtr += offset; + if (offset < 0) { + return -1; + } else if ((bufPtr[0] == firstFH[0]) && (bufPtr[1] == firstFH[1]) && ((bufPtr[2] & 0xfc) == (firstFH[2] & 0xfc))) { + /* want to return number of bytes per frame, NOT counting the padding + * byte, so subtract one if padFlag == 1 */ + if ((firstFH[2] >> 1) & 0x01) + bufPtr--; + return bufPtr - buf; + } + bufPtr += 3; + nBytes -= (offset + 3); + }; + + return -1; +} + +/************************************************************************************** + * Function: MP3GetLastFrameInfo + * + * Description: get info about last MP3 frame decoded (number of sampled + *decoded, sample rate, bitrate, etc.) + * + * Inputs: valid MP3 decoder instance pointer (HMP3Decoder) + * pointer to MP3FrameInfo struct + * + * Outputs: filled-in MP3FrameInfo struct + * + * Return: none + * + * Notes: call this right after calling MP3Decode + **************************************************************************************/ +void MP3GetLastFrameInfo(HMP3Decoder hMP3Decoder, MP3FrameInfo *mp3FrameInfo) { + MP3DecInfo *mp3DecInfo = (MP3DecInfo *) hMP3Decoder; + + if (!mp3DecInfo || mp3DecInfo->layer != 3) { + mp3FrameInfo->bitrate = 0; + mp3FrameInfo->nChans = 0; + mp3FrameInfo->samprate = 0; + mp3FrameInfo->bitsPerSample = 0; + mp3FrameInfo->outputSamps = 0; + mp3FrameInfo->layer = 0; + mp3FrameInfo->version = 0; + } else { + mp3FrameInfo->bitrate = mp3DecInfo->bitrate; + mp3FrameInfo->nChans = mp3DecInfo->nChans; + mp3FrameInfo->samprate = mp3DecInfo->samprate; + mp3FrameInfo->bitsPerSample = 16; + mp3FrameInfo->outputSamps = + mp3DecInfo->nChans * (int) samplesPerFrameTab[mp3DecInfo->version][mp3DecInfo->layer - 1]; + mp3FrameInfo->layer = mp3DecInfo->layer; + mp3FrameInfo->version = mp3DecInfo->version; + } +} + +/************************************************************************************** + * Function: MP3GetNextFrameInfo + * + * Description: parse MP3 frame header + * + * Inputs: valid MP3 decoder instance pointer (HMP3Decoder) + * pointer to MP3FrameInfo struct + * pointer to buffer containing valid MP3 frame header (located + *using MP3FindSyncWord(), above) + * + * Outputs: filled-in MP3FrameInfo struct + * + * Return: error code, defined in mp3dec.h (0 means no error, < 0 means + *error) + **************************************************************************************/ +int MP3GetNextFrameInfo(HMP3Decoder hMP3Decoder, MP3FrameInfo *mp3FrameInfo, unsigned char *buf) { + MP3DecInfo *mp3DecInfo = (MP3DecInfo *) hMP3Decoder; + + if (!mp3DecInfo) + return ERR_MP3_NULL_POINTER; + + if (UnpackFrameHeader(mp3DecInfo, buf) == -1 || mp3DecInfo->layer != 3) + return ERR_MP3_INVALID_FRAMEHEADER; + + MP3GetLastFrameInfo(mp3DecInfo, mp3FrameInfo); + + return ERR_MP3_NONE; +} + +/************************************************************************************** + * Function: MP3ClearBadFrame + * + * Description: zero out pcm buffer if error decoding MP3 frame + * + * Inputs: mp3DecInfo struct with correct frame size parameters filled in + * pointer pcm output buffer + * + * Outputs: zeroed out pcm buffer + * + * Return: none + **************************************************************************************/ +static void MP3ClearBadFrame(MP3DecInfo *mp3DecInfo, short *outbuf) { + int i; + + if (!mp3DecInfo) + return; + + for (i = 0; i < mp3DecInfo->nGrans * mp3DecInfo->nGranSamps * mp3DecInfo->nChans; i++) + outbuf[i] = 0; +} + +/************************************************************************************** + * Function: MP3Decode + * + * Description: decode one frame of MP3 data + * + * Inputs: valid MP3 decoder instance pointer (HMP3Decoder) + * double pointer to buffer of MP3 data (containing headers + + *mainData) number of valid bytes remaining in inbuf pointer to outbuf, big + *enough to hold one frame of decoded PCM samples flag indicating whether MP3 + *data is normal MPEG format (useSize = 0) or reformatted as "self-contained" + *frames (useSize = 1) + * + * Outputs: PCM data in outbuf, interleaved LRLRLR... if stereo + * number of output samples = nGrans * nGranSamps * nChans + * updated inbuf pointer, updated bytesLeft + * + * Return: error code, defined in mp3dec.h (0 means no error, < 0 means + *error) + * + * Notes: switching useSize on and off between frames in the same stream + * is not supported (bit reservoir is not maintained if useSize + *on) + **************************************************************************************/ +int MP3Decode(HMP3Decoder hMP3Decoder, unsigned char **inbuf, int *bytesLeft, short *outbuf, int useSize) { + int offset, bitOffset, mainBits, gr, ch, fhBytes, siBytes, freeFrameBytes; + int prevBitOffset, sfBlockBits, huffBlockBits; + unsigned char *mainPtr; + MP3DecInfo *mp3DecInfo = (MP3DecInfo *) hMP3Decoder; + + if (!mp3DecInfo) + return ERR_MP3_NULL_POINTER; + + /* unpack frame header */ + fhBytes = UnpackFrameHeader(mp3DecInfo, *inbuf); + if (fhBytes < 0) + return ERR_MP3_INVALID_FRAMEHEADER; /* don't clear outbuf since we don't + know size (failed to parse header) */ + *inbuf += fhBytes; + + /* unpack side info */ + siBytes = UnpackSideInfo(mp3DecInfo, *inbuf); + if (siBytes < 0) { + MP3ClearBadFrame(mp3DecInfo, outbuf); + return ERR_MP3_INVALID_SIDEINFO; + } + *inbuf += siBytes; + *bytesLeft -= (fhBytes + siBytes); + + /* if free mode, need to calculate bitrate and nSlots manually, based on frame + * size */ + if (mp3DecInfo->bitrate == 0 || mp3DecInfo->freeBitrateFlag) { + if (!mp3DecInfo->freeBitrateFlag) { + /* first time through, need to scan for next sync word and figure out + * frame size */ + mp3DecInfo->freeBitrateFlag = 1; + mp3DecInfo->freeBitrateSlots = MP3FindFreeSync(*inbuf, *inbuf - fhBytes - siBytes, *bytesLeft); + if (mp3DecInfo->freeBitrateSlots < 0) { + MP3ClearBadFrame(mp3DecInfo, outbuf); + return ERR_MP3_FREE_BITRATE_SYNC; + } + freeFrameBytes = mp3DecInfo->freeBitrateSlots + fhBytes + siBytes; + mp3DecInfo->bitrate = (freeFrameBytes * mp3DecInfo->samprate * 8) / (mp3DecInfo->nGrans * mp3DecInfo->nGranSamps); + } + mp3DecInfo->nSlots = mp3DecInfo->freeBitrateSlots + CheckPadBit(mp3DecInfo); /* add pad byte, if required */ + } + + /* useSize != 0 means we're getting reformatted (RTP) packets (see RFC 3119) + * - calling function assembles "self-contained" MP3 frames by shifting any + * main_data from the bit reservoir (in previous frames) to AFTER the sync + * word and side info + * - calling function should set mainDataBegin to 0, and tell us exactly how + * large this frame is (in bytesLeft) + */ + if (useSize) { + mp3DecInfo->nSlots = *bytesLeft; + if (mp3DecInfo->mainDataBegin != 0 || mp3DecInfo->nSlots <= 0) { + /* error - non self-contained frame, or missing frame (size <= 0), could + * do loss concealment here */ + MP3ClearBadFrame(mp3DecInfo, outbuf); + return ERR_MP3_INVALID_FRAMEHEADER; + } + + /* can operate in-place on reformatted frames */ + mp3DecInfo->mainDataBytes = mp3DecInfo->nSlots; + mainPtr = *inbuf; + *inbuf += mp3DecInfo->nSlots; + *bytesLeft -= (mp3DecInfo->nSlots); + } else { + /* out of data - assume last or truncated frame */ + if (mp3DecInfo->nSlots > *bytesLeft) { + MP3ClearBadFrame(mp3DecInfo, outbuf); + return ERR_MP3_INDATA_UNDERFLOW; + } + + /* fill main data buffer with enough new data for this frame */ + if (mp3DecInfo->mainDataBytes >= mp3DecInfo->mainDataBegin) { + /* adequate "old" main data available (i.e. bit reservoir) */ + memmove(mp3DecInfo->mainBuf, mp3DecInfo->mainBuf + mp3DecInfo->mainDataBytes - mp3DecInfo->mainDataBegin, + mp3DecInfo->mainDataBegin); + memcpy(mp3DecInfo->mainBuf + mp3DecInfo->mainDataBegin, *inbuf, mp3DecInfo->nSlots); + + mp3DecInfo->mainDataBytes = mp3DecInfo->mainDataBegin + mp3DecInfo->nSlots; + *inbuf += mp3DecInfo->nSlots; + *bytesLeft -= (mp3DecInfo->nSlots); + mainPtr = mp3DecInfo->mainBuf; + } else { + /* not enough data in bit reservoir from previous frames (perhaps starting + * in middle of file) */ + memcpy(mp3DecInfo->mainBuf + mp3DecInfo->mainDataBytes, *inbuf, mp3DecInfo->nSlots); + mp3DecInfo->mainDataBytes += mp3DecInfo->nSlots; + *inbuf += mp3DecInfo->nSlots; + *bytesLeft -= (mp3DecInfo->nSlots); + MP3ClearBadFrame(mp3DecInfo, outbuf); + return ERR_MP3_MAINDATA_UNDERFLOW; + } + } + bitOffset = 0; + mainBits = mp3DecInfo->mainDataBytes * 8; + + /* decode one complete frame */ + for (gr = 0; gr < mp3DecInfo->nGrans; gr++) { + for (ch = 0; ch < mp3DecInfo->nChans; ch++) { + /* unpack scale factors and compute size of scale factor block */ + prevBitOffset = bitOffset; + offset = UnpackScaleFactors(mp3DecInfo, mainPtr, &bitOffset, mainBits, gr, ch); + + sfBlockBits = 8 * offset - prevBitOffset + bitOffset; + huffBlockBits = mp3DecInfo->part23Length[gr][ch] - sfBlockBits; + mainPtr += offset; + mainBits -= sfBlockBits; + + if (offset < 0 || mainBits < huffBlockBits) { + MP3ClearBadFrame(mp3DecInfo, outbuf); + return ERR_MP3_INVALID_SCALEFACT; + } + + /* decode Huffman code words */ + prevBitOffset = bitOffset; + offset = DecodeHuffman(mp3DecInfo, mainPtr, &bitOffset, huffBlockBits, gr, ch); + if (offset < 0) { + MP3ClearBadFrame(mp3DecInfo, outbuf); + return ERR_MP3_INVALID_HUFFCODES; + } + + mainPtr += offset; + mainBits -= (8 * offset - prevBitOffset + bitOffset); + } + + /* dequantize coefficients, decode stereo, reorder short blocks */ + if (Dequantize(mp3DecInfo, gr) < 0) { + MP3ClearBadFrame(mp3DecInfo, outbuf); + return ERR_MP3_INVALID_DEQUANTIZE; + } + + /* alias reduction, inverse MDCT, overlap-add, frequency inversion */ + for (ch = 0; ch < mp3DecInfo->nChans; ch++) { + if (IMDCT(mp3DecInfo, gr, ch) < 0) { + MP3ClearBadFrame(mp3DecInfo, outbuf); + return ERR_MP3_INVALID_IMDCT; + } + } + + /* subband transform - if stereo, interleaves pcm LRLRLR */ + if (Subband(mp3DecInfo, outbuf + gr * mp3DecInfo->nGranSamps * mp3DecInfo->nChans) < 0) { + MP3ClearBadFrame(mp3DecInfo, outbuf); + return ERR_MP3_INVALID_SUBBAND; + } + } + return ERR_MP3_NONE; +} +#endif \ No newline at end of file diff --git a/esphome/components/satellite1/media_player/mp3_decoder.h b/esphome/components/satellite1/media_player/mp3_decoder.h new file mode 100644 index 00000000..aa88c40a --- /dev/null +++ b/esphome/components/satellite1/media_player/mp3_decoder.h @@ -0,0 +1,385 @@ +#ifdef USE_ESP_IDF + +#ifndef MP3_DECODER_H_ +#define MP3_DECODER_H_ + +#include +#include +#include + +#define ASSERT(x) /* do nothing */ + +/* determining MAINBUF_SIZE: + * max mainDataBegin = (2^9 - 1) bytes (since 9-bit offset) = 511 + * max nSlots (concatenated with mainDataBegin bytes from before) = 1440 - 9 - + * 4 + 1 = 1428 511 + 1428 = 1939, round up to 1940 (4-byte align) + */ +#define MAINBUF_SIZE 1940 + +#define MAX_NGRAN 2 /* max granules */ +#define MAX_NCHAN 2 /* max channels */ +#define MAX_NSAMP 576 /* max samples per channel, per granule */ + +/* map to 0,1,2 to make table indexing easier */ +typedef enum { MPEG1 = 0, MPEG2 = 1, MPEG25 = 2 } MPEGVersion; + +#define MAX_SCFBD 4 /* max scalefactor bands per channel */ +#define NGRANS_MPEG1 2 +#define NGRANS_MPEG2 1 + +/* 11-bit syncword if MPEG 2.5 extensions are enabled */ +/* +#define SYNCWORDH 0xff +#define SYNCWORDL 0xe0 +*/ + +/* 12-bit syncword if MPEG 1,2 only are supported */ +#define SYNCWORDH 0xff +#define SYNCWORDL 0xf0 + +#ifndef MAX +#define MAX(a, b) ((a) > (b) ? (a) : (b)) +#endif + +#ifndef MIN +#define MIN(a, b) ((a) < (b) ? (a) : (b)) +#endif + +typedef long long Word64; + +static __inline Word64 MADD64(Word64 sum64, int x, int y) { return (sum64 + ((long long) x * y)); } + +static __inline int MULSHIFT32(int x, int y) { + /* important rules for smull RdLo, RdHi, Rm, Rs: + * RdHi and Rm can't be the same register + * RdLo and Rm can't be the same register + * RdHi and RdLo can't be the same register + * Note: Rs determines early termination (leading sign bits) so if you want to + * specify which operand is Rs, put it in the SECOND argument (y) For inline + * assembly, x and y are not assumed to be R0, R1 so it shouldn't matter which + * one is returned. (If this were a function call, returning y (R1) would + * require an extra "mov r0, r1") + */ + int ret; + asm volatile("mulsh %0, %1, %2" : "=r"(ret) : "r"(x), "r"(y)); + return ret; +} + +static __inline int FASTABS(int x) { + int ret; + asm volatile("abs %0, %1" : "=r"(ret) : "r"(x)); + return ret; +} + +static __inline Word64 SAR64(Word64 x, int n) { return x >> n; } + +static __inline int CLZ(int x) { return __builtin_clz(x); } + +/* clip to range [-2^n, 2^n - 1] */ +#define CLIP_2N(y, n) \ + { \ + int sign = (y) >> 31; \ + if (sign != (y) >> (n)) { \ + (y) = sign ^ ((1 << (n)) - 1); \ + } \ + } + +#define SIBYTES_MPEG1_MONO 17 +#define SIBYTES_MPEG1_STEREO 32 +#define SIBYTES_MPEG2_MONO 9 +#define SIBYTES_MPEG2_STEREO 17 + +/* number of fraction bits for pow43Tab (see comments there) */ +#define POW43_FRACBITS_LOW 22 +#define POW43_FRACBITS_HIGH 12 + +#define DQ_FRACBITS_OUT 25 /* number of fraction bits in output of dequant */ +#define IMDCT_SCALE 2 /* additional scaling (by sqrt(2)) for fast IMDCT36 */ + +#define HUFF_PAIRTABS 32 +#define BLOCK_SIZE 18 +#define NBANDS 32 +#define MAX_REORDER_SAMPS ((192 - 126) * 3) /* largest critical band for short blocks (see sfBandTable) */ +#define VBUF_LENGTH (17 * 2 * NBANDS) /* for double-sized vbuf FIFO */ + +/* map these to the corresponding 2-bit values in the frame header */ +typedef enum { + Stereo = 0x00, /* two independent channels, but L and R frames might have + different # of bits */ + Joint = 0x01, /* coupled channels - layer III: mix of M-S and intensity, + Layers I/II: intensity and direct coding only */ + Dual = 0x02, /* two independent channels, L and R always have exactly 1/2 the + total bitrate */ + Mono = 0x03 /* one channel */ +} StereoMode; + +typedef struct _SFBandTable { + short l[23]; + short s[14]; +} SFBandTable; + +typedef struct _BitStreamInfo { + unsigned char *bytePtr; + unsigned int iCache; + int cachedBits; + int nBytes; +} BitStreamInfo; + +typedef struct _FrameHeader { + MPEGVersion ver; /* version ID */ + int layer; /* layer index (1, 2, or 3) */ + int crc; /* CRC flag: 0 = disabled, 1 = enabled */ + int brIdx; /* bitrate index (0 - 15) */ + int srIdx; /* sample rate index (0 - 2) */ + int paddingBit; /* padding flag: 0 = no padding, 1 = single pad byte */ + int privateBit; /* unused */ + StereoMode sMode; /* mono/stereo mode */ + int modeExt; /* used to decipher joint stereo mode */ + int copyFlag; /* copyright flag: 0 = no, 1 = yes */ + int origFlag; /* original flag: 0 = copy, 1 = original */ + int emphasis; /* deemphasis mode */ + int CRCWord; /* CRC word (16 bits, 0 if crc not enabled) */ + + const SFBandTable *sfBand; +} FrameHeader; + +typedef struct _SideInfoSub { + int part23Length; /* number of bits in main data */ + int nBigvals; /* 2x this = first set of Huffman cw's (maximum amplitude can be + > 1) */ + int globalGain; /* overall gain for dequantizer */ + int sfCompress; /* unpacked to figure out number of bits in scale factors */ + int winSwitchFlag; /* window switching flag */ + int blockType; /* block type */ + int mixedBlock; /* 0 = regular block (all short or long), 1 = mixed block */ + int tableSelect[3]; /* index of Huffman tables for the big values regions */ + int subBlockGain[3]; /* subblock gain offset, relative to global gain */ + int region0Count; /* 1+region0Count = num scale factor bands in first region + of bigvals */ + int region1Count; /* 1+region1Count = num scale factor bands in second region + of bigvals */ + int preFlag; /* for optional high frequency boost */ + int sfactScale; /* scaling of the scalefactors */ + int count1TableSelect; /* index of Huffman table for quad codewords */ +} SideInfoSub; + +typedef struct _SideInfo { + int mainDataBegin; + int privateBits; + int scfsi[MAX_NCHAN][MAX_SCFBD]; /* 4 scalefactor bands per channel */ + + SideInfoSub sis[MAX_NGRAN][MAX_NCHAN]; +} SideInfo; + +typedef struct { + int cbType; /* pure long = 0, pure short = 1, mixed = 2 */ + int cbEndS[3]; /* number nonzero short cb's, per subbblock */ + int cbEndSMax; /* max of cbEndS[] */ + int cbEndL; /* number nonzero long cb's */ +} CriticalBandInfo; + +typedef struct _DequantInfo { + int workBuf[MAX_REORDER_SAMPS]; /* workbuf for reordering short blocks */ + CriticalBandInfo cbi[MAX_NCHAN]; /* filled in dequantizer, used in joint + stereo reconstruction */ +} DequantInfo; + +typedef struct _HuffmanInfo { + int huffDecBuf[MAX_NCHAN][MAX_NSAMP]; /* used both for decoded Huffman values + and dequantized coefficients */ + int nonZeroBound[MAX_NCHAN]; /* number of coeffs in huffDecBuf[ch] which can + be > 0 */ + int gb[MAX_NCHAN]; /* minimum number of guard bits in huffDecBuf[ch] */ +} HuffmanInfo; + +typedef enum _HuffTabType { noBits, oneShot, loopNoLinbits, loopLinbits, quadA, quadB, invalidTab } HuffTabType; + +typedef struct _HuffTabLookup { + int linBits; + HuffTabType tabType; +} HuffTabLookup; + +typedef struct _IMDCTInfo { + int outBuf[MAX_NCHAN][BLOCK_SIZE][NBANDS]; /* output of IMDCT */ + int overBuf[MAX_NCHAN][MAX_NSAMP / 2]; /* overlap-add buffer (by symmetry, + only need 1/2 size) */ + int numPrevIMDCT[MAX_NCHAN]; /* how many IMDCT's calculated in this channel on + prev. granule */ + int prevType[MAX_NCHAN]; + int prevWinSwitch[MAX_NCHAN]; + int gb[MAX_NCHAN]; +} IMDCTInfo; + +typedef struct _BlockCount { + int nBlocksLong; + int nBlocksTotal; + int nBlocksPrev; + int prevType; + int prevWinSwitch; + int currWinSwitch; + int gbIn; + int gbOut; +} BlockCount; + +/* max bits in scalefactors = 5, so use char's to save space */ +typedef struct _ScaleFactorInfoSub { + char l[23]; /* [band] */ + char s[13][3]; /* [band][window] */ +} ScaleFactorInfoSub; + +/* used in MPEG 2, 2.5 intensity (joint) stereo only */ +typedef struct _ScaleFactorJS { + int intensityScale; + int slen[4]; + int nr[4]; +} ScaleFactorJS; + +typedef struct _ScaleFactorInfo { + ScaleFactorInfoSub sfis[MAX_NGRAN][MAX_NCHAN]; + ScaleFactorJS sfjs; +} ScaleFactorInfo; + +/* NOTE - could get by with smaller vbuf if memory is more important than speed + * (in Subband, instead of replicating each block in FDCT32 you would do a + * memmove on the last 15 blocks to shift them down one, a hardware style FIFO) + */ +typedef struct _SubbandInfo { + int vbuf[MAX_NCHAN * VBUF_LENGTH]; /* vbuf for fast DCT-based synthesis PQMF - double size + for speed (no modulo indexing) */ + int vindex; /* internal index for tracking position in vbuf */ +} SubbandInfo; + +/* bitstream.c */ +void SetBitstreamPointer(BitStreamInfo *bsi, int nBytes, unsigned char *buf); +unsigned int GetBits(BitStreamInfo *bsi, int nBits); +int CalcBitsUsed(BitStreamInfo *bsi, unsigned char *startBuf, int startOffset); + +/* dequant.c, dqchan.c, stproc.c */ +int DequantChannel(int *sampleBuf, int *workBuf, int *nonZeroBound, FrameHeader *fh, SideInfoSub *sis, + ScaleFactorInfoSub *sfis, CriticalBandInfo *cbi); +void MidSideProc(int x[MAX_NCHAN][MAX_NSAMP], int nSamps, int mOut[2]); +void IntensityProcMPEG1(int x[MAX_NCHAN][MAX_NSAMP], int nSamps, FrameHeader *fh, ScaleFactorInfoSub *sfis, + CriticalBandInfo *cbi, int midSideFlag, int mixFlag, int mOut[2]); +void IntensityProcMPEG2(int x[MAX_NCHAN][MAX_NSAMP], int nSamps, FrameHeader *fh, ScaleFactorInfoSub *sfis, + CriticalBandInfo *cbi, ScaleFactorJS *sfjs, int midSideFlag, int mixFlag, int mOut[2]); + +/* dct32.c */ +// about 1 ms faster in RAM, but very large +void FDCT32(int *x, int *d, int offset, int oddBlock, + int gb); // __attribute__ ((section (".data"))); + +/* hufftabs.c */ +extern const HuffTabLookup huffTabLookup[HUFF_PAIRTABS]; +extern const int huffTabOffset[HUFF_PAIRTABS]; +extern const unsigned short huffTable[]; +extern const unsigned char quadTable[64 + 16]; +extern const int quadTabOffset[2]; +extern const int quadTabMaxBits[2]; + +void PolyphaseMono(short *pcm, int *vbuf, const int *coefBase); +void PolyphaseStereo(short *pcm, int *vbuf, const int *coefBase); + +/* trigtabs.c */ +extern const uint32_t imdctWin[4][36]; +extern const int ISFMpeg1[2][7]; +extern const int ISFMpeg2[2][2][16]; +extern const int ISFIIP[2][2]; +extern const uint32_t csa[8][2]; +extern const int coef32[31]; +extern const uint32_t polyCoef[264]; + +typedef struct _MP3DecInfo { + /* pointers to platform-specific data structures */ + void *FrameHeaderPS; + void *SideInfoPS; + void *ScaleFactorInfoPS; + void *HuffmanInfoPS; + void *DequantInfoPS; + void *IMDCTInfoPS; + void *SubbandInfoPS; + + /* buffer which must be large enough to hold largest possible main_data + * section */ + unsigned char mainBuf[MAINBUF_SIZE]; + + /* special info for "free" bitrate files */ + int freeBitrateFlag; + int freeBitrateSlots; + + /* user-accessible info */ + int bitrate; + int nChans; + int samprate; + int nGrans; /* granules per frame */ + int nGranSamps; /* samples per granule */ + int nSlots; + int layer; + MPEGVersion version; + + int mainDataBegin; + int mainDataBytes; + + int part23Length[MAX_NGRAN][MAX_NCHAN]; + +} MP3DecInfo; + +MP3DecInfo *AllocateBuffers(void); +void FreeBuffers(MP3DecInfo *mp3DecInfo); +int CheckPadBit(MP3DecInfo *mp3DecInfo); +int UnpackFrameHeader(MP3DecInfo *mp3DecInfo, unsigned char *buf); +int UnpackSideInfo(MP3DecInfo *mp3DecInfo, unsigned char *buf); +int DecodeHuffman(MP3DecInfo *mp3DecInfo, unsigned char *buf, int *bitOffset, int huffBlockBits, int gr, int ch); +int Dequantize(MP3DecInfo *mp3DecInfo, int gr); +int IMDCT(MP3DecInfo *mp3DecInfo, int gr, int ch); +int UnpackScaleFactors(MP3DecInfo *mp3DecInfo, unsigned char *buf, int *bitOffset, int bitsAvail, int gr, int ch); +int Subband(MP3DecInfo *mp3DecInfo, short *pcmBuf); + +extern const int samplerateTab[3][3]; +extern const short bitrateTab[3][3][15]; +extern const short samplesPerFrameTab[3][3]; +extern const short bitsPerSlotTab[3]; +extern const short sideBytesTab[3][2]; +extern const short slotTab[3][3][15]; +extern const SFBandTable sfBandTable[3][3]; + +typedef void *HMP3Decoder; + +enum { + ERR_MP3_NONE = 0, + ERR_MP3_INDATA_UNDERFLOW = -1, + ERR_MP3_MAINDATA_UNDERFLOW = -2, + ERR_MP3_FREE_BITRATE_SYNC = -3, + ERR_MP3_OUT_OF_MEMORY = -4, + ERR_MP3_NULL_POINTER = -5, + ERR_MP3_INVALID_FRAMEHEADER = -6, + ERR_MP3_INVALID_SIDEINFO = -7, + ERR_MP3_INVALID_SCALEFACT = -8, + ERR_MP3_INVALID_HUFFCODES = -9, + ERR_MP3_INVALID_DEQUANTIZE = -10, + ERR_MP3_INVALID_IMDCT = -11, + ERR_MP3_INVALID_SUBBAND = -12, + + ERR_UNKNOWN = -9999 +}; + +typedef struct _MP3FrameInfo { + int bitrate; + int nChans; + int samprate; + int bitsPerSample; + int outputSamps; + int layer; + int version; +} MP3FrameInfo; + +/* public API */ +HMP3Decoder MP3InitDecoder(void); +void MP3FreeDecoder(HMP3Decoder hMP3Decoder); +int MP3Decode(HMP3Decoder hMP3Decoder, unsigned char **inbuf, int *bytesLeft, short *outbuf, int useSize); + +void MP3GetLastFrameInfo(HMP3Decoder hMP3Decoder, MP3FrameInfo *mp3FrameInfo); +int MP3GetNextFrameInfo(HMP3Decoder hMP3Decoder, MP3FrameInfo *mp3FrameInfo, unsigned char *buf); +int MP3FindSyncWord(unsigned char *buf, int nBytes); + +#endif // MP3_DECODER_H_ +#endif \ No newline at end of file diff --git a/esphome/components/satellite1/media_player/nabu_media_player.cpp b/esphome/components/satellite1/media_player/nabu_media_player.cpp new file mode 100644 index 00000000..4bea613e --- /dev/null +++ b/esphome/components/satellite1/media_player/nabu_media_player.cpp @@ -0,0 +1,663 @@ +#ifdef USE_ESP_IDF + +#include "nabu_media_player.h" + +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +#include "esp_dsp.h" + +#ifdef USE_OTA +#include "esphome/components/ota/ota_backend.h" +#endif + +namespace esphome { +namespace nabu { + +// TODO: +// - Align TLS certification defines with the http_request component +// - Clean up process around playing back local media files +// - Create a registry of media files in Python +// - Add a yaml action to play a specific media file +// +// +// Framework: +// - Media player that can handle two streams; one for media and one for announcements +// - If played together, they are mixed with the announcement stream staying at full volume +// - The media audio is scaled, if necessary, to avoid clipping when mixing an announcement stream +// - The media audio can be further ducked via the ``set_ducking_reduction`` function +// - Each stream is handled by an ``AudioPipeline`` object with three parts/tasks +// - ``AudioReader`` handles reading from an HTTP source or from a PROGMEM flash set at compile time +// - ``AudioDecoder`` handles decoding the audio file. All formats are limited to two channels and 16 bits per sample +// - FLAC +// - WAV +// - MP3 (based on the libhelix decoder - a random mp3 file may be incompatible) +// - ``AudioResampler`` handles converting the sample rate to the configured output sample rate and converting mono +// to stereo +// - The quality is not good, and it is slow! Please use audio at the configured sample rate to avoid these issues +// - Each task will always run once started, but they will not doing anything until they are needed +// - FreeRTOS Event Groups make up the inter-task communication +// - The ``AudioPipeline`` sets up an output ring buffer for the Reader and Decoder parts. The next part/task +// automatically pulls from the previous ring buffer +// - The streams are mixed together in the ``AudioMixer`` task +// - Each stream has a corresponding input buffer that the ``AudioResampler`` feeds directly +// - Pausing the media stream is done here +// - Media stream ducking is done here +// - The output ring buffer feeds the ``speaker_task`` directly. It is kept small intentionally to avoid latency when +// pausing +// - Audio output is handled by the ``speaker_task``. It configures the I2S bus and copies audio from the mixer's +// output ring buffer to the DMA buffers +// - Media player commands are received by the ``control`` function. The commands are added to the +// ``media_control_command_queue_`` to be processed in the component's loop +// - Starting a stream intializes the appropriate pipeline or stops it if it is already running +// - Volume and mute commands are achieved by the ``mute``, ``unmute``, ``set_volume`` functions. Volume changes use +// an ``audio_dac`` component if configured. If one isn't, software volume control is used. +// - Volume commands are ignored if the media control queue is full to avoid crashing when the track wheel is spun +// fast +// - Pausing is sent to the ``AudioMixer`` task. It only effects the media stream. +// - The components main loop performs housekeeping: +// - It reads the media control queue and processes it directly +// - It watches the state of speaker and mixer tasks +// - It determines the overall state of the media player by considering the state of each pipeline +// - announcement playback takes highest priority + +static const size_t QUEUE_LENGTH = 20; + +static const uint8_t NUMBER_OF_CHANNELS = 2; // Hard-coded expectation of stereo (2 channel) audio +static const size_t DMA_BUFFER_SIZE = 512; +static const size_t SAMPLES_IN_ONE_DMA_BUFFER = DMA_BUFFER_SIZE * NUMBER_OF_CHANNELS; +static const size_t DMA_BUFFERS_COUNT = 4; +static const size_t SAMPLES_IN_ALL_DMA_BUFFERS = SAMPLES_IN_ONE_DMA_BUFFER * DMA_BUFFERS_COUNT; + +static const UBaseType_t MEDIA_PIPELINE_TASK_PRIORITY = 1; +static const UBaseType_t ANNOUNCEMENT_PIPELINE_TASK_PRIORITY = 1; +static const UBaseType_t MIXER_TASK_PRIORITY = 10; +static const UBaseType_t SPEAKER_TASK_PRIORITY = 23; + +static const size_t TASK_DELAY_MS = 10; + +static const float FIRST_BOOT_DEFAULT_VOLUME = 0.5f; + +enum SpeakerTaskNotificationBits : uint32_t { + COMMAND_START = (1 << 0), // Starts the main task purpose + COMMAND_STOP = (1 << 1), // stops the main task +}; + +static const char *const TAG = "nabu_media_player"; + +void NabuMediaPlayer::setup() { + state = media_player::MEDIA_PLAYER_STATE_IDLE; + + this->media_control_command_queue_ = xQueueCreate(QUEUE_LENGTH, sizeof(MediaCallCommand)); + this->speaker_event_queue_ = xQueueCreate(QUEUE_LENGTH, sizeof(TaskEvent)); + + this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); + + VolumeRestoreState volume_restore_state; + if (this->pref_.load(&volume_restore_state)) { + this->set_volume_(volume_restore_state.volume); + this->set_mute_state_(volume_restore_state.is_muted); + } else { + this->set_volume_(FIRST_BOOT_DEFAULT_VOLUME); + this->set_mute_state_(false); + } + +#ifdef USE_OTA + ota::get_global_ota_callback()->add_on_state_callback( + [this](ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) { + if (state == ota::OTA_STARTED) { + if (this->speaker_task_handle_ != nullptr) { + vTaskSuspend(this->speaker_task_handle_); + } + if (this->audio_mixer_ != nullptr) { + this->audio_mixer_->suspend_task(); + } + if (this->media_pipeline_ != nullptr) { + this->media_pipeline_->suspend_tasks(); + } + if (this->announcement_pipeline_ != nullptr) { + this->announcement_pipeline_->suspend_tasks(); + } + } else if (state == ota::OTA_ERROR) { + if (this->speaker_task_handle_ != nullptr) { + vTaskResume(this->speaker_task_handle_); + } + if (this->audio_mixer_ != nullptr) { + this->audio_mixer_->resume_task(); + } + if (this->media_pipeline_ != nullptr) { + this->media_pipeline_->resume_tasks(); + } + if (this->announcement_pipeline_ != nullptr) { + this->announcement_pipeline_->resume_tasks(); + } + } + }); +#endif + + ESP_LOGI(TAG, "Set up nabu media player"); +} + +esp_err_t NabuMediaPlayer::start_i2s_driver_() { + if (!this->claim_i2s_access()) { + return ESP_ERR_INVALID_STATE; // Waiting for another i2s component to return lock + } + + i2s_driver_config_t config = this->get_i2s_cfg(); + if (!this->install_i2s_driver(config)) { + this->release_i2s_access(); + return ESP_ERR_INVALID_STATE; + } + return ESP_OK; +} + +void NabuMediaPlayer::dump_config(){ + this->dump_i2s_settings(); +} + + +void NabuMediaPlayer::speaker_task(void *params) { + NabuMediaPlayer *this_speaker = (NabuMediaPlayer *) params; + + TaskEvent event; + esp_err_t err; + + while (true) { + uint32_t notification_bits = 0; + xTaskNotifyWait(ULONG_MAX, // clear all bits at start of wait + ULONG_MAX, // clear all bits after waiting + ¬ification_bits, // notifcation value after wait is finished + portMAX_DELAY); // how long to wait + + if (notification_bits & SpeakerTaskNotificationBits::COMMAND_START) { + event.type = EventType::STARTING; + xQueueSend(this_speaker->speaker_event_queue_, &event, portMAX_DELAY); + + ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + int16_t *buffer = allocator.allocate(SAMPLES_IN_ALL_DMA_BUFFERS); + + if (buffer == nullptr) { + event.type = EventType::WARNING; + event.err = ESP_ERR_NO_MEM; + xQueueSend(this_speaker->speaker_event_queue_, &event, portMAX_DELAY); + } else { + err = this_speaker->start_i2s_driver_(); + + if (err != ESP_OK) { + event.type = EventType::WARNING; + event.err = err; + xQueueSend(this_speaker->speaker_event_queue_, &event, portMAX_DELAY); + } else { + event.type = EventType::STARTED; + xQueueSend(this_speaker->speaker_event_queue_, &event, portMAX_DELAY); + + while (true) { + notification_bits = ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(0)); + + if (notification_bits & SpeakerTaskNotificationBits::COMMAND_STOP) { + break; + } + + size_t bytes_read = 0; + size_t bytes_to_read = sizeof(int16_t) * SAMPLES_IN_ALL_DMA_BUFFERS; + bytes_read = + this_speaker->audio_mixer_->read((uint8_t *) buffer, bytes_to_read, pdMS_TO_TICKS(TASK_DELAY_MS)); + + if (bytes_read > 0) { + size_t bytes_written; + +#ifdef USE_AUDIO_DAC + if (this_speaker->audio_dac_ == nullptr) +#endif + { // Fallback to software volume control if an audio dac isn't available + int16_t volume_scale_factor = + this_speaker->software_volume_scale_factor_; // Atomic read, so thread safe + if (volume_scale_factor < INT16_MAX) { +#if defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32) + dsps_mulc_s16_ae32(buffer, buffer, bytes_read / sizeof(int16_t), volume_scale_factor, 1, 1); +#else + dsps_mulc_s16_ansi(buffer, buffer, bytes_read / sizeof(int16_t), volume_scale_factor, 1, 1); +#endif + } + } + + if (this_speaker->bits_per_sample_ == I2S_BITS_PER_SAMPLE_16BIT) { + i2s_write(this_speaker->parent_->get_port(), buffer, bytes_read, &bytes_written, portMAX_DELAY); + } else { + i2s_write_expand(this_speaker->parent_->get_port(), buffer, bytes_read, I2S_BITS_PER_SAMPLE_16BIT, + this_speaker->bits_per_sample_, &bytes_written, portMAX_DELAY); + } + + if (bytes_written != bytes_read) { + event.type = EventType::WARNING; + event.err = ESP_ERR_INVALID_SIZE; + xQueueSend(this_speaker->speaker_event_queue_, &event, portMAX_DELAY); + } else { + event.type = EventType::RUNNING; + xQueueSend(this_speaker->speaker_event_queue_, &event, 0); + } + + } else { + //i2s_zero_dma_buffer(this_speaker->parent_->get_port()); + + event.type = EventType::IDLE; + xQueueSend(this_speaker->speaker_event_queue_, &event, 0); + } + } + + //i2s_zero_dma_buffer(this_speaker->parent_->get_port()); + + event.type = EventType::STOPPING; + xQueueSend(this_speaker->speaker_event_queue_, &event, portMAX_DELAY); + + allocator.deallocate(buffer, SAMPLES_IN_ALL_DMA_BUFFERS); + //i2s_stop(this_speaker->parent_->get_port()); + //i2s_driver_uninstall(this_speaker->parent_->get_port()); + this_speaker->uninstall_i2s_driver(); + //this_speaker->parent_->unlock(); + + event.type = EventType::STOPPED; + xQueueSend(this_speaker->speaker_event_queue_, &event, portMAX_DELAY); + } + } + } + } +} + +esp_err_t NabuMediaPlayer::start_pipeline_(AudioPipelineType type, bool url) { + esp_err_t err = ESP_OK; + + if (this->audio_mixer_ == nullptr) { + this->audio_mixer_ = make_unique(); + err = this->audio_mixer_->start("mixer", MIXER_TASK_PRIORITY); + if (err != ESP_OK) { + return err; + } + } + + if (this->speaker_task_handle_ == nullptr) { + xTaskCreate(NabuMediaPlayer::speaker_task, "speaker_task", 3072, (void *) this, SPEAKER_TASK_PRIORITY, + &this->speaker_task_handle_); + if (this->speaker_task_handle_ == nullptr) { + return ESP_FAIL; + } + } + + xTaskNotify(this->speaker_task_handle_, SpeakerTaskNotificationBits::COMMAND_START, eSetValueWithoutOverwrite); + + if (type == AudioPipelineType::MEDIA) { + if (this->media_pipeline_ == nullptr) { + this->media_pipeline_ = make_unique(this->audio_mixer_.get(), type); + } + + if (url) { + err = this->media_pipeline_->start(this->media_url_.value(), this->sample_rate_, "media", + MEDIA_PIPELINE_TASK_PRIORITY); + } else { + err = this->media_pipeline_->start(this->media_file_.value(), this->sample_rate_, "media", + MEDIA_PIPELINE_TASK_PRIORITY); + } + + if (this->is_paused_) { + CommandEvent command_event; + command_event.command = CommandEventType::RESUME_MEDIA; + this->audio_mixer_->send_command(&command_event); + } + this->is_paused_ = false; + } else if (type == AudioPipelineType::ANNOUNCEMENT) { + if (this->announcement_pipeline_ == nullptr) { + this->announcement_pipeline_ = make_unique(this->audio_mixer_.get(), type); + } + + if (url) { + err = this->announcement_pipeline_->start(this->announcement_url_.value(), this->sample_rate_, "ann", + ANNOUNCEMENT_PIPELINE_TASK_PRIORITY); + } else { + err = this->announcement_pipeline_->start(this->announcement_file_.value(), this->sample_rate_, "ann", + ANNOUNCEMENT_PIPELINE_TASK_PRIORITY); + } + } + + return err; +} + +void NabuMediaPlayer::watch_media_commands_() { + MediaCallCommand media_command; + CommandEvent command_event; + esp_err_t err = ESP_OK; + + if (xQueueReceive(this->media_control_command_queue_, &media_command, 0) == pdTRUE) { + if (media_command.new_url.has_value() && media_command.new_url.value()) { + if (media_command.announce.has_value() && media_command.announce.value()) { + err = this->start_pipeline_(AudioPipelineType::ANNOUNCEMENT, true); + } else { + err = this->start_pipeline_(AudioPipelineType::MEDIA, true); + } + } + + if (media_command.new_file.has_value() && media_command.new_file.value()) { + if (media_command.announce.has_value() && media_command.announce.value()) { + err = this->start_pipeline_(AudioPipelineType::ANNOUNCEMENT, false); + } else { + err = this->start_pipeline_(AudioPipelineType::MEDIA, false); + } + } + + if (err != ESP_OK) { + ESP_LOGE(TAG, "Error starting the audio pipeline: %s", esp_err_to_name(err)); + this->status_set_error(); + } else { + this->status_clear_error(); + } + + if (media_command.volume.has_value()) { + this->set_volume_(media_command.volume.value()); + this->publish_state(); + } + + if (media_command.command.has_value()) { + switch (media_command.command.value()) { + case media_player::MEDIA_PLAYER_COMMAND_PLAY: + if ((this->audio_mixer_ != nullptr) && this->is_paused_) { + command_event.command = CommandEventType::RESUME_MEDIA; + this->audio_mixer_->send_command(&command_event); + } + this->is_paused_ = false; + break; + case media_player::MEDIA_PLAYER_COMMAND_PAUSE: + if ((this->audio_mixer_ != nullptr) && !this->is_paused_) { + command_event.command = CommandEventType::PAUSE_MEDIA; + this->audio_mixer_->send_command(&command_event); + } + this->is_paused_ = true; + break; + case media_player::MEDIA_PLAYER_COMMAND_STOP: + command_event.command = CommandEventType::STOP; + if (media_command.announce.has_value() && media_command.announce.value()) { + if (this->announcement_pipeline_ != nullptr) { + this->announcement_pipeline_->stop(); + } + } else { + if (this->media_pipeline_ != nullptr) { + this->media_pipeline_->stop(); + } + } + break; + case media_player::MEDIA_PLAYER_COMMAND_TOGGLE: + if ((this->audio_mixer_ != nullptr) && this->is_paused_) { + command_event.command = CommandEventType::RESUME_MEDIA; + this->audio_mixer_->send_command(&command_event); + this->is_paused_ = false; + } else if (this->audio_mixer_ != nullptr) { + command_event.command = CommandEventType::PAUSE_MEDIA; + this->audio_mixer_->send_command(&command_event); + this->is_paused_ = true; + } + break; + case media_player::MEDIA_PLAYER_COMMAND_MUTE: { + this->set_mute_state_(true); + + this->publish_state(); + break; + } + case media_player::MEDIA_PLAYER_COMMAND_UNMUTE: + this->set_mute_state_(false); + this->publish_state(); + break; + case media_player::MEDIA_PLAYER_COMMAND_VOLUME_UP: + this->set_volume_(std::min(1.0f, this->volume + this->volume_increment_)); + this->publish_state(); + break; + case media_player::MEDIA_PLAYER_COMMAND_VOLUME_DOWN: + this->set_volume_(std::max(0.0f, this->volume - this->volume_increment_)); + this->publish_state(); + break; + default: + break; + } + } + } +} + +void NabuMediaPlayer::watch_speaker_() { + TaskEvent event; + while (xQueueReceive(this->speaker_event_queue_, &event, 0)) { + switch (event.type) { + case EventType::STARTING: + ESP_LOGD(TAG, "Starting Media Player Speaker"); + break; + case EventType::STARTED: + ESP_LOGD(TAG, "Started Media Player Speaker"); + break; + case EventType::IDLE: + break; + case EventType::RUNNING: + break; + case EventType::STOPPING: + ESP_LOGD(TAG, "Stopping Media Player Speaker"); + break; + case EventType::STOPPED: + xQueueReset(this->speaker_event_queue_); + + ESP_LOGD(TAG, "Stopped Media Player Speaker"); + break; + case EventType::WARNING: + ESP_LOGW(TAG, "Error writing to I2S: %s", esp_err_to_name(event.err)); + this->status_set_warning(); + break; + } + } +} + +void NabuMediaPlayer::watch_mixer_() { + TaskEvent event; + if (this->audio_mixer_ != nullptr) { + while (this->audio_mixer_->read_event(&event)) + if (event.type == EventType::WARNING) { + ESP_LOGD(TAG, "Mixer encountered an error: %s", esp_err_to_name(event.err)); + this->status_set_error(); + } + } +} + +void NabuMediaPlayer::loop() { + this->watch_media_commands_(); + this->watch_mixer_(); + this->watch_speaker_(); + + // Determine state of the media player + media_player::MediaPlayerState old_state = this->state; + + if (this->announcement_pipeline_ != nullptr) + this->announcement_pipeline_state_ = this->announcement_pipeline_->get_state(); + + if (this->media_pipeline_ != nullptr) + this->media_pipeline_state_ = this->media_pipeline_->get_state(); + + if (this->media_pipeline_state_ == AudioPipelineState::ERROR_READING) { + ESP_LOGE(TAG, "The media pipeline's file reader encountered an error."); + } else if (this->media_pipeline_state_ == AudioPipelineState::ERROR_DECODING) { + ESP_LOGE(TAG, "The media pipeline's audio decoder encountered an error."); + } else if (this->media_pipeline_state_ == AudioPipelineState::ERROR_RESAMPLING) { + ESP_LOGE(TAG, "The media pipeline's audio resampler encountered an error."); + } + + if (this->announcement_pipeline_state_ == AudioPipelineState::ERROR_READING) { + ESP_LOGE(TAG, "The announcement pipeline's file reader encountered an error."); + } else if (this->announcement_pipeline_state_ == AudioPipelineState::ERROR_DECODING) { + ESP_LOGE(TAG, "The announcement pipeline's audio decoder encountered an error."); + } else if (this->announcement_pipeline_state_ == AudioPipelineState::ERROR_RESAMPLING) { + ESP_LOGE(TAG, "The announcement pipeline's audio resampler encountered an error."); + } + + if (this->announcement_pipeline_state_ != AudioPipelineState::STOPPED) { + this->state = media_player::MEDIA_PLAYER_STATE_ANNOUNCING; + } else { + if (this->media_pipeline_state_ == AudioPipelineState::STOPPED) { + this->state = media_player::MEDIA_PLAYER_STATE_IDLE; + } else if (this->is_paused_) { + this->state = media_player::MEDIA_PLAYER_STATE_PAUSED; + } else { + this->state = media_player::MEDIA_PLAYER_STATE_PLAYING; + } + } + + if (this->state != old_state) { + this->publish_state(); + } +} + +void NabuMediaPlayer::set_ducking_reduction(uint8_t decibel_reduction, float duration) { + if (this->audio_mixer_ != nullptr) { + CommandEvent command_event; + command_event.command = CommandEventType::DUCK; + command_event.decibel_reduction = decibel_reduction; + + // Convert the duration in seconds to number of samples, accounting for the sample rate and number of channels + command_event.transition_samples = static_cast(duration * this->sample_rate_ * NUMBER_OF_CHANNELS); + this->audio_mixer_->send_command(&command_event); + } +} + +void NabuMediaPlayer::control(const media_player::MediaPlayerCall &call) { + MediaCallCommand media_command; + + if (call.get_announcement().has_value() && call.get_announcement().value()) { + media_command.announce = true; + } else { + media_command.announce = false; + } + + if (call.get_media_url().has_value()) { + std::string new_uri = call.get_media_url().value(); + + media_command.new_url = true; + if (call.get_announcement().has_value() && call.get_announcement().value()) { + this->announcement_url_ = new_uri; + } else { + this->media_url_ = new_uri; + } + xQueueSend(this->media_control_command_queue_, &media_command, portMAX_DELAY); + return; + } + + if (call.get_local_media_file().has_value()) { + if (call.get_announcement().has_value() && call.get_announcement().value()) { + this->announcement_file_ = call.get_local_media_file().value(); + } else { + this->media_file_ = call.get_local_media_file().value(); + } + media_command.new_file = true; + xQueueSend(this->media_control_command_queue_, &media_command, portMAX_DELAY); + return; + } + + if (call.get_volume().has_value()) { + media_command.volume = call.get_volume().value(); + // Wait 0 ticks for queue to be free, volume sets aren't that important! + xQueueSend(this->media_control_command_queue_, &media_command, 0); + return; + } + + if (call.get_command().has_value()) { + media_command.command = call.get_command().value(); + TickType_t ticks_to_wait = portMAX_DELAY; + if ((call.get_command().value() == media_player::MEDIA_PLAYER_COMMAND_VOLUME_UP) || + (call.get_command().value() == media_player::MEDIA_PLAYER_COMMAND_VOLUME_DOWN)) { + ticks_to_wait = 0; // Wait 0 ticks for queue to be free, volume sets aren't that important! + } + xQueueSend(this->media_control_command_queue_, &media_command, ticks_to_wait); + return; + } +} + +media_player::MediaPlayerTraits NabuMediaPlayer::get_traits() { + auto traits = media_player::MediaPlayerTraits(); + traits.set_supports_pause(true); + traits.get_supported_formats().push_back( + media_player::MediaPlayerSupportedFormat{.format = "flac", + .sample_rate = 48000, + .num_channels = 2, + .purpose = media_player::MediaPlayerFormatPurpose::PURPOSE_DEFAULT, + .sample_bytes = 2}); + traits.get_supported_formats().push_back( + media_player::MediaPlayerSupportedFormat{.format = "flac", + .sample_rate = 48000, + .num_channels = 1, + .purpose = media_player::MediaPlayerFormatPurpose::PURPOSE_ANNOUNCEMENT, + .sample_bytes = 2}); + return traits; +}; + +void NabuMediaPlayer::save_volume_restore_state_() { + VolumeRestoreState volume_restore_state; + volume_restore_state.volume = this->volume; + volume_restore_state.is_muted = this->is_muted_; + this->pref_.save(&volume_restore_state); +} + +void NabuMediaPlayer::set_mute_state_(bool mute_state) { +#ifdef USE_AUDIO_DAC + if (this->audio_dac_ != nullptr) { + if (mute_state) { + this->audio_dac_->set_mute_on(); + } else { + this->audio_dac_->set_mute_off(); + } + } else +#endif + { // Fall back to software mute control if there is no audio_dac or if it isn't configured + if (mute_state) { + this->software_volume_scale_factor_ = 0; + } else if (this->software_volume_scale_factor_ == 0){ + this->set_volume_(this->volume, false); // restore previous volume + } + } + + bool old_mute_state = this->is_muted_; + this->is_muted_ = mute_state; + + this->save_volume_restore_state_(); + + if (old_mute_state != mute_state) { + if (mute_state) { + this->defer([this]() { this->mute_trigger_->trigger(); }); + } else { + this->defer([this]() { this->unmute_trigger_->trigger(); }); + } + } +} + +void NabuMediaPlayer::set_volume_(float volume, bool publish) { + // Remap the volume to fit with in the configured limits + float bounded_volume = remap(volume, 0.0f, 1.0f, this->volume_min_, this->volume_max_); + +#ifdef USE_AUDIO_DAC + if (this->audio_dac_ != nullptr) { + this->audio_dac_->set_volume(bounded_volume); + } else +#endif + { // Fall back to software volume control if there is no audio_dac or if it isn't configured + // Use the decibel reduction table from audio_mixer.h for software volume control + ssize_t decibel_index = remap(bounded_volume, 1.0f, 0.0f, 0, decibel_reduction_table.size() - 1); + this->software_volume_scale_factor_ = decibel_reduction_table[decibel_index]; + } + + if (publish) { + this->volume = volume; + this->save_volume_restore_state_(); + } + + // Turn on the mute state if the volume is effectively zero, off otherwise + if (volume < 0.001) { + this->set_mute_state_(true); + } else { + this->set_mute_state_(false); + } + + this->defer([this, volume]() { this->volume_trigger_->trigger(volume); }); +} + +} // namespace nabu +} // namespace esphome +#endif diff --git a/esphome/components/satellite1/media_player/nabu_media_player.h b/esphome/components/satellite1/media_player/nabu_media_player.h new file mode 100644 index 00000000..0c1b1967 --- /dev/null +++ b/esphome/components/satellite1/media_player/nabu_media_player.h @@ -0,0 +1,173 @@ +#pragma once + +#ifdef USE_ESP_IDF + +#include "audio_mixer.h" +#include "audio_pipeline.h" + +#ifdef USE_AUDIO_DAC +#include "esphome/components/audio_dac/audio_dac.h" +#endif +#include "esphome/components/i2s_audio/i2s_audio.h" +#include "esphome/components/media_player/media_player.h" + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/preferences.h" + +#include +#include + +#include + +namespace esphome { +namespace nabu { + +static const uint8_t DAC_PAGE_SELECTION_REGISTER = 0x00; +static const uint8_t DAC_LEFT_MUTE_REGISTER = 0x12; +static const uint8_t DAC_RIGHT_MUTE_REGISTER = 0x13; +static const uint8_t DAC_LEFT_VOLUME_REGISTER = 0x41; +static const uint8_t DAC_RIGHT_VOLUME_REGISTER = 0x42; + +static const uint8_t DAC_VOLUME_PAGE = 0x00; +static const uint8_t DAC_MUTE_PAGE = 0x01; + +static const uint8_t DAC_MUTE_COMMAND = 0x40; +static const uint8_t DAC_UNMUTE_COMMAND = 0x00; + +struct MediaCallCommand { + optional command; + optional volume; + optional announce; + optional new_url; + optional new_file; +}; + +struct VolumeRestoreState { + float volume; + bool is_muted; +}; + +class NabuMediaPlayer : public Component, public media_player::MediaPlayer, public i2s_audio::I2SWriter { + public: + float get_setup_priority() const override { return esphome::setup_priority::LATE; } + void setup() override; + void dump_config() override; + void loop() override; + + // MediaPlayer implementations + media_player::MediaPlayerTraits get_traits() override; + bool is_muted() const override { return this->is_muted_; } + + /// @brief Sets the ducking level for the media stream in the mixer + /// @param decibel_reduction (uint8_t) The dB reduction level. For example, 0 is no change, 10 is a reduction by 10 dB + /// @param duration (float) The duration (in seconds) for transitioning to the new ducking level + void set_ducking_reduction(uint8_t decibel_reduction, float duration); + + // Percentage to increase or decrease the volume for volume up or volume down commands + void set_volume_increment(float volume_increment) { this->volume_increment_ = volume_increment; } + + void set_volume_max(float volume_max) { this->volume_max_ = volume_max; } + void set_volume_min(float volume_min) { this->volume_min_ = volume_min; } + +#ifdef USE_AUDIO_DAC + void set_audio_dac(audio_dac::AudioDac *audio_dac) { this->audio_dac_ = audio_dac; } +#endif + + Trigger<> *get_mute_trigger() const { return this->mute_trigger_; } + Trigger<> *get_unmute_trigger() const { return this->unmute_trigger_; } + Trigger *get_volume_trigger() const { return this->volume_trigger_; } + + protected: + // Receives commands from HA or from the voice assistant component + // Sends commands to the media_control_commanda_queue_ + void control(const media_player::MediaPlayerCall &call) override; + + /// @brief Updates this->volume and saves volume/mute state to flash for restortation if publish is true. + void set_volume_(float volume, bool publish = true); + + /// @brief Sets the mute state. Restores previous volume if unmuting. Always saves volume/mute state to flash for + /// restoration. + /// @param mute_state If true, audio will be muted. If false, audio will be unmuted + void set_mute_state_(bool mute_state); + + /// @brief Saves the current volume and mute state to the flash for restoration. + void save_volume_restore_state_(); + + esp_err_t start_i2s_driver_(); + + // Reads commands from media_control_command_queue_. Starts pipelines and mixer if necessary. + void watch_media_commands_(); + + std::unique_ptr media_pipeline_; + std::unique_ptr announcement_pipeline_; + std::unique_ptr audio_mixer_; + + // Monitors the mixer task + void watch_mixer_(); + + // Starts the ``type`` pipeline with a ``url`` or file. Starts the mixer, pipeline, and speaker tasks if necessary. + // Unpauses if starting media in paused state + esp_err_t start_pipeline_(AudioPipelineType type, bool url); + + AudioPipelineState media_pipeline_state_{AudioPipelineState::STOPPED}; + AudioPipelineState announcement_pipeline_state_{AudioPipelineState::STOPPED}; + + void watch_speaker_(); + + static void speaker_task(void *params); + TaskHandle_t speaker_task_handle_{nullptr}; + QueueHandle_t speaker_event_queue_; + + optional media_url_{}; // only modified by control function + optional announcement_url_{}; // only modified by control function + optional media_file_{}; // only modified by control fucntion + optional announcement_file_{}; // only modified by control fucntion + + QueueHandle_t media_control_command_queue_; + + bool is_paused_{false}; + bool is_muted_{false}; + + // The amount to change the volume on volume up/down commands + float volume_increment_; + + float volume_max_; + float volume_min_; + +#ifdef USE_AUDIO_DAC + audio_dac::AudioDac *audio_dac_{nullptr}; +#endif + + int16_t software_volume_scale_factor_; // Q15 fixed point scale factor + + // Used to save volume/mute state for restoration on reboot + ESPPreferenceObject pref_; + + Trigger<> *mute_trigger_ = new Trigger<>(); + Trigger<> *unmute_trigger_ = new Trigger<>(); + Trigger *volume_trigger_ = new Trigger(); +}; + +template class DuckingSetAction : public Action, public Parented { + TEMPLATABLE_VALUE(uint8_t, decibel_reduction) + TEMPLATABLE_VALUE(float, duration) + void play(Ts... x) override { + this->parent_->set_ducking_reduction(this->decibel_reduction_.value(x...), this->duration_.value(x...)); + } +}; + +// template class PlayLocalMediaAction : public Action, public Parented { +// TEMPLATABLE_VALUE(media_player::MediaFile, media_file) +// // public: +// // void set_media_file(media_player::MediaFile media_file) { this->media_file_ = media_file; } +// void play(Ts... x) override { +// this->parent_->make_call().set_local_media_file(this->media_file_.value(x...)).perform(); } +// // protected: +// // media_player::MediaFile media_file_; +// }; + +} // namespace nabu +} // namespace esphome + +#endif diff --git a/esphome/components/satellite1/media_player/resampler.cpp b/esphome/components/satellite1/media_player/resampler.cpp new file mode 100755 index 00000000..c0829950 --- /dev/null +++ b/esphome/components/satellite1/media_player/resampler.cpp @@ -0,0 +1,516 @@ +//////////////////////////////////////////////////////////////////////////// +// **** RESAMPLER **** // +// Sinc-based Audio Resampling // +// Copyright (c) 2006 - 2023 David Bryant. // +// All Rights Reserved. // +// Distributed under the BSD Software License (see license.txt) // +//////////////////////////////////////////////////////////////////////////// + +// resampler.c + +#ifdef USE_ESP_IDF + +#include "resampler.h" + +#include "esphome/core/helpers.h" +#include "esp_dsp.h" + +static void init_filter(Resample *cxt, float *filter, float fraction, float lowpass_ratio); +static float subsample(Resample *cxt, float *source, float offset); + +// Initialize a resampler context with the specified characteristics. The returned context pointer +// is used for all subsequent calls to the resampler (and should not be dereferenced). A NULL +// return indicates an error. For the flags parameter, note that SUBSAMPLE_INTERPOLATE and +// BLACKMAN_HARRIS are recommended for most applications. The parameters are: +// +// numChannels: the number of audio channels present +// +// numTaps: the number of taps for each sinc interpolation filter +// - must be a multiple of 4, from 4 - 1024 taps +// - affects quality by controlling cutoff sharpness of filters +// - linearly affects memory usage and CPU load of resampling +// +// numFilters: the number of sinc filters generated +// - must be 2 - 1024 +// - affects quality of interpolated filtering +// - linearly affects memory usage of resampler +// +// lowpassRatio: enable lowpass by specifying ratio < 1.0 (relative to input samples); +// this is required for downsampling, optional otherwise +// +// flags: mask for optional feature configuration: +// +// SUBSAMPLE_INTERPOLATE: interpolate values from adjacent filters +// - generally recommended except in special situations +// - approximately floats the CPU load +// +// BLACKMAN_HARRIS: use 4-term Blackman Harris window function +// - generally recommended except in special situations +// - if not specified, the default window is Hann (raised cosine) +// which has steeper cutoff but poorer stopband rejection +// +// INCLUDE_LOWPASS: include lowpass in sinc interpolation filters +// - lowpassRatio specifies frequency as a ratio to input samples +// - required for downsampling, optional otherwise +// +// Notes: +// +// 1. The same resampling instance can be used for upsampling, downsampling, or simple (near-unity) +// resampling (e.g., for asynchronous sample rate conversion, phase shifting, or filtering). The +// behavior is controlled by the "ratio" parameter during the actual resample processing call. +// To prevent aliasing, it's important to specify a lowpassRatio if the resampler will be used +// for any significant degree of downsampling (ratio < 1.0), but the lowpass can also be used +// independently without any rate conversion (or even upsampling). +// +// 2. When the context is initialized (or reset) the sample histories are filled with silence such +// that the resampler is ready to generate output immediately. However, this also means that there +// is an implicit signal delay equal to half the tap length of the sinc filters in samples. If +// zero delay is desired then that many samples can be ignored, or the resampleAdvancePosition() +// function can be used to bypass them. Also, at the end of processing an equal length of silence +// must be appended to the input audio to align the output with the actual input. +// +// 3. Both the number of interpolation filters and the number of taps per filter directly control +// the fidelity of the resampling. The filter length has an approximately linear affect on the +// the CPU load consumed by the resampler, and also on the memory requirement (both for the +// filters themselves and also for the sample history storage). On the other hand, the number +// of filters allocated primarily affects just the memory footprint (it has little affect on CPU +// load and so can be large on systems with lots of RAM). + +Resample *resampleInit(int numChannels, int numTaps, int numFilters, float lowpassRatio, int flags) { + Resample *cxt = (Resample *) calloc(1, sizeof(Resample)); + int i; + + if (lowpassRatio > 0.0 && lowpassRatio < 1.0) + flags |= INCLUDE_LOWPASS; + else { + flags &= ~INCLUDE_LOWPASS; + lowpassRatio = 1.0; + } + + if ((numTaps & 3) || numTaps <= 0 || numTaps > 1024) { + fprintf(stderr, "must 4-1024 filter taps, and a multiple of 4!\n"); + return NULL; + } + + if (numFilters < 2 || numFilters > 1024) { + fprintf(stderr, "must be 2-1024 filters!\n"); + return NULL; + } + + cxt->numChannels = numChannels; + cxt->numSamples = numTaps * 16; + cxt->numFilters = numFilters; + cxt->numTaps = numTaps; + cxt->flags = flags; + + // note that we actually have one more than the specified number of filters + esphome::ExternalRAMAllocator float_allocator(esphome::ExternalRAMAllocator::ALLOW_FAILURE); + cxt->filters = (float **) calloc(cxt->numFilters + 1, sizeof(float *)); + cxt->tempFilter = float_allocator.allocate(numTaps); + // cxt->tempFilter = malloc (numTaps * sizeof (float)); + + for (i = 0; i <= cxt->numFilters; ++i) { + cxt->filters[i] = float_allocator.allocate(cxt->numTaps); + memset(cxt->filters[i], 0, cxt->numTaps * sizeof(float)); + // cxt->filters [i] = calloc (cxt->numTaps, sizeof (float)); + init_filter(cxt, cxt->filters[i], (float) i / cxt->numFilters, lowpassRatio); + } + + free(cxt->tempFilter); + cxt->tempFilter = NULL; + cxt->buffers = (float **) calloc(numChannels, sizeof(float *)); + + for (i = 0; i < numChannels; ++i) { + cxt->buffers[i] = float_allocator.allocate(cxt->numSamples); + memset(cxt->buffers[i], 0, cxt->numSamples * sizeof(float)); + // cxt->buffers [i] = calloc (cxt->numSamples, sizeof (float)); + } + + cxt->outputOffset = numTaps / 2; + cxt->inputIndex = numTaps; + + return cxt; +} + +// Reset a resampler context to its initialized state. Specifically, any history is discarded +// and this should be used when an audio "flush" or other discontinuity occurs. + +void resampleReset(Resample *cxt) { + int i; + + for (i = 0; i < cxt->numChannels; ++i) + memset(cxt->buffers[i], 0, cxt->numSamples * sizeof(float)); + + cxt->outputOffset = cxt->numTaps / 2; + cxt->inputIndex = cxt->numTaps; +} + +// Run the resampler context at the specified output ratio and return both the number of input +// samples consumed and output samples generated (in the ResampleResult structure). Over time +// the average number of output samples will be equal to the number of input samples multiplied +// by the given ratio, but of course in a single call only an integer number of samples can be +// generated. The numInputFrames parameter indicates the number of samples available at "input" +// and the numOutputFrames indicates the number of samples at "output" that can be written. The +// resampling proceeds until EITHER the input is exhausted or space at the output is exhausted +// (there is no other limit). +// +// This is the "non-interleaved" version of the resampler where the audio sample buffers for +// different channels are passed in as an array of float pointers. There is also an +// "interleaved" version (see below). + +ResampleResult resampleProcess(Resample *cxt, const float *const *input, int numInputFrames, float *const *output, + int numOutputFrames, float ratio) { + int half_taps = cxt->numTaps / 2, i; + ResampleResult res = {0, 0}; + + while (numOutputFrames > 0) { + if (cxt->outputOffset >= cxt->inputIndex - half_taps) { + if (numInputFrames > 0) { + if (cxt->inputIndex == cxt->numSamples) { + for (i = 0; i < cxt->numChannels; ++i) + memmove(cxt->buffers[i], cxt->buffers[i] + cxt->numSamples - cxt->numTaps, cxt->numTaps * sizeof(float)); + + cxt->outputOffset -= cxt->numSamples - cxt->numTaps; + cxt->inputIndex -= cxt->numSamples - cxt->numTaps; + } + + for (i = 0; i < cxt->numChannels; ++i) + cxt->buffers[i][cxt->inputIndex] = input[i][res.input_used]; + + cxt->inputIndex++; + res.input_used++; + numInputFrames--; + } else + break; + } else { + for (i = 0; i < cxt->numChannels; ++i) + output[i][res.output_generated] = subsample(cxt, cxt->buffers[i], cxt->outputOffset); + + cxt->outputOffset += (1.0 / ratio); + res.output_generated++; + numOutputFrames--; + } + } + + return res; +} + +// This is the "interleaved" version of the resampler where the audio samples for different +// channels are passed in sequence in a single buffer. There is also a "non-interleaved" +// version for independent buffers, which is otherwise identical (see above). + +ResampleResult resampleProcessInterleaved(Resample *cxt, const float *input, int numInputFrames, float *output, + int numOutputFrames, float ratio) { + int half_taps = cxt->numTaps / 2, i; + ResampleResult res = {0, 0}; + + while (numOutputFrames > 0) { + if (cxt->outputOffset >= cxt->inputIndex - half_taps) { + if (numInputFrames > 0) { + if (cxt->inputIndex == cxt->numSamples) { + for (i = 0; i < cxt->numChannels; ++i) + memmove(cxt->buffers[i], cxt->buffers[i] + cxt->numSamples - cxt->numTaps, cxt->numTaps * sizeof(float)); + + cxt->outputOffset -= cxt->numSamples - cxt->numTaps; + cxt->inputIndex -= cxt->numSamples - cxt->numTaps; + } + + for (i = 0; i < cxt->numChannels; ++i) + cxt->buffers[i][cxt->inputIndex] = *input++; + + cxt->inputIndex++; + res.input_used++; + numInputFrames--; + } else + break; + } else { + for (i = 0; i < cxt->numChannels; ++i) + *output++ = subsample(cxt, cxt->buffers[i], cxt->outputOffset); + + cxt->outputOffset += (1.0 / ratio); + res.output_generated++; + numOutputFrames--; + } + } + + return res; +} + +// These two functions are not required for any application, but might be useful. Essentially +// they allow a "dry run" of the resampler to determine beforehand how many input samples +// would be consumed to generate a given output, or how many samples would be generated with +// a given input. +// +// Note that there is a tricky edge-case here for ratios just over 1.0. If a query is made as +// to how many input samples are required to generate a given output, that does NOT necessarily +// mean that exactly that many samples will be generated with the indicated input (specifically +// an extra sample might be generated). Therefore it is important to restrict the output with +// numOutputFrames if an exact output count is desired (don't just assume the input count can +// exactly determine the output count). + +unsigned int resampleGetRequiredSamples(Resample *cxt, int numOutputFrames, float ratio) { + int half_taps = cxt->numTaps / 2; + int input_index = cxt->inputIndex; + float offset = cxt->outputOffset; + ResampleResult res = {0, 0}; + + while (numOutputFrames > 0) { + if (offset >= input_index - half_taps) { + if (input_index == cxt->numSamples) { + offset -= cxt->numSamples - cxt->numTaps; + input_index -= cxt->numSamples - cxt->numTaps; + } + + input_index++; + res.input_used++; + } else { + offset += (1.0 / ratio); + numOutputFrames--; + } + } + + return res.input_used; +} + +unsigned int resampleGetExpectedOutput(Resample *cxt, int numInputFrames, float ratio) { + int half_taps = cxt->numTaps / 2; + int input_index = cxt->inputIndex; + float offset = cxt->outputOffset; + ResampleResult res = {0, 0}; + + while (1) { + if (offset >= input_index - half_taps) { + if (numInputFrames > 0) { + if (input_index == cxt->numSamples) { + offset -= cxt->numSamples - cxt->numTaps; + input_index -= cxt->numSamples - cxt->numTaps; + } + + input_index++; + numInputFrames--; + } else + break; + } else { + offset += (1.0 / ratio); + res.output_generated++; + } + } + + return res.output_generated; +} + +// Advance the resampler output without generating any output, with the units referenced +// to the input sample array. This can be used to temporally align the output to the input +// (by specifying half the sinc filter tap width), and it can also be used to introduce a +// phase shift. The resampler cannot be reversed. + +void resampleAdvancePosition(Resample *cxt, float delta) { + if (delta < 0.0) + fprintf(stderr, "resampleAdvancePosition() can only advance forward!\n"); + else + cxt->outputOffset += delta; +} + +// Get the subsample position of the resampler. This is initialized to zero when the +// resampler is started (or reset) and moves around zero as the resampler processes +// audio. Obtaining this value is generally not required, but can be useful for +// applications that need accurate phase information from the resampler such as +// asynchronous sample rate converter (ASRC) implementations. The units are relative +// to the input samples, and a negative value indicates that an output sample is +// ready (i.e., can be generated with no further input read). +// +// To fully understand the meaning of the position value the following C-like +// pseudo-code for the resampler is presented. Note that this code has the length +// of the sinc filters and the actual interpolation abstracted away (like they +// abstracted away from the user of the library): +// +// while (numOutputFrames > 0) { +// if (position < 0.0) { +// write (output); +// numOutputFrames--; +// position += (1.0 / ratio); +// } +// else if (numInputFrames > 0) { +// read (input); +// numInputFrames--; +// position -= 1.0; +// } +// else +// break; +// } + +float resampleGetPosition(Resample *cxt) { return cxt->outputOffset + (cxt->numTaps / 2.0) - cxt->inputIndex; } + +// Free all resources associated with the resampler context, including the context pointer +// itself. Do not use the context after this call. + +void resampleFree(Resample *cxt) { + int i; + + for (i = 0; i <= cxt->numFilters; ++i) + free(cxt->filters[i]); + + free(cxt->filters); + + for (i = 0; i < cxt->numChannels; ++i) + free(cxt->buffers[i]); + + free(cxt->buffers); + free(cxt); +} + +// This is the basic convolution operation that is the core of the resampler and utilizes the +// bulk of the CPU load (assuming reasonably long filters). The first version is the canonical +// form, followed by three variations that may or may not be faster depending on your compiler, +// options, and system. Try 'em and use the fastest, or rewrite them using SIMD. Note that on +// gcc and clang, -Ofast can make a huge difference. + +#if 1 // Version 1 (canonical) +// static float apply_filter (float *A, float *B, int num_taps) +// { +// float sum = 0.0; + +// do sum += *A++ * *B++; +// while (--num_taps); + +// return sum; +// } +static float apply_filter(float *A, float *B, int num_taps) { + float sum; + dsps_dotprod_f32_aes3(A, B, &sum, num_taps); + // dsps_dotprod_f32_ansi(A, B, &sum, num_taps); + return sum; +} +#endif + +#if 0 // Version 2 (2x unrolled loop) +static float apply_filter (float *A, float *B, int num_taps) +{ + int num_loops = num_taps >> 1; + float sum = 0.0; + + do { + sum += (A[0] * B[0]) + (A[1] * B[1]); + A += 2; B += 2; + } while (--num_loops); + + return sum; +} +#endif + +#if 0 // Version 3 (4x unrolled loop) +static float apply_filter (float *A, float *B, int num_taps) +{ + int num_loops = num_taps >> 2; + float sum = 0.0; + + do { + sum += (A[0] * B[0]) + (A[1] * B[1]) + (A[2] * B[2]) + (A[3] * B[3]); + A += 4; B += 4; + } while (--num_loops); + + return sum; +} +#endif + +#if 0 // Version 4 (outside-in order, may be more accurate) +static float apply_filter (float *A, float *B, int num_taps) +{ + int i = num_taps - 1; + float sum = 0.0; + + do { + sum += (A[0] * B[0]) + (A[i] * B[i]); + A++; B++; + } while ((i -= 2) > 0); + + return sum; +} +#endif + +#ifndef M_PI +#define M_PI 3.14159265358979324 +#endif + +static void init_filter(Resample *cxt, float *filter, float fraction, float lowpass_ratio) { + const float a0 = 0.35875; + const float a1 = 0.48829; + const float a2 = 0.14128; + const float a3 = 0.01168; + float filter_sum = 0.0; + int i; + + // "dist" is the absolute distance from the sinc maximum to the filter tap to be calculated, in radians + // "ratio" is that distance divided by half the tap count such that it reaches π at the window extremes + + // Note that with this scaling, the odd terms of the Blackman-Harris calculation appear to be negated + // with respect to the reference formula version. + + for (i = 0; i < cxt->numTaps; ++i) { + float dist = fabs((cxt->numTaps / 2 - 1) + fraction - i) * M_PI; + float ratio = dist / (cxt->numTaps / 2); + float value; + + if (dist != 0.0) { + value = sin(dist * lowpass_ratio) / (dist * lowpass_ratio); + + if (cxt->flags & BLACKMAN_HARRIS) + value *= a0 + a1 * cos(ratio) + a2 * cos(2 * ratio) + a3 * cos(3 * ratio); + else + value *= 0.5 * (1.0 + cos(ratio)); // Hann window + } else + value = 1.0; + + filter_sum += cxt->tempFilter[i] = value; + } + + // filter should have unity DC gain + + float scaler = 1.0 / filter_sum, error = 0.0; + + for (i = cxt->numTaps / 2; i < cxt->numTaps; i = cxt->numTaps - i - (i >= cxt->numTaps / 2)) { + filter[i] = (cxt->tempFilter[i] *= scaler) - error; + error += filter[i] - cxt->tempFilter[i]; + } +} + +static float subsample_no_interpolate(Resample *cxt, float *source, float offset) { + source += (int) floor(offset); + offset -= floor(offset); + + if (offset == 0.0 && !(cxt->flags & INCLUDE_LOWPASS)) + return *source; + + return apply_filter(cxt->filters[(int) floor(offset * cxt->numFilters + 0.5)], source - cxt->numTaps / 2 + 1, + cxt->numTaps); +} + +static float subsample_interpolate(Resample *cxt, float *source, float offset) { + float sum1, sum2; + int i; + + source += (int) floor(offset); + offset -= floor(offset); + + if (offset == 0.0 && !(cxt->flags & INCLUDE_LOWPASS)) + return *source; + + i = (int) floor(offset *= cxt->numFilters); + sum1 = apply_filter(cxt->filters[i], source - cxt->numTaps / 2 + 1, cxt->numTaps); + + if ((offset -= i) == 0.0 && !(cxt->flags & INCLUDE_LOWPASS)) + return sum1; + + sum2 = apply_filter(cxt->filters[i + 1], source - cxt->numTaps / 2 + 1, cxt->numTaps); + + return sum2 * offset + sum1 * (1.0 - offset); +} + +static float subsample(Resample *cxt, float *source, float offset) { + if (cxt->flags & SUBSAMPLE_INTERPOLATE) + return subsample_interpolate(cxt, source, offset); + else + return subsample_no_interpolate(cxt, source, offset); +} + +#endif \ No newline at end of file diff --git a/esphome/components/satellite1/media_player/resampler.h b/esphome/components/satellite1/media_player/resampler.h new file mode 100755 index 00000000..16f003fe --- /dev/null +++ b/esphome/components/satellite1/media_player/resampler.h @@ -0,0 +1,57 @@ +//////////////////////////////////////////////////////////////////////////// +// **** RESAMPLER **** // +// Sinc-based Audio Resampling // +// Copyright (c) 2006 - 2023 David Bryant. // +// All Rights Reserved. // +// Distributed under the BSD Software License (see license.txt) // +//////////////////////////////////////////////////////////////////////////// + +// resampler.h +#pragma once + +#ifdef USE_ESP_IDF + +#include +#include +#include +#include +#include +#include + +#include "esphome/core/helpers.h" + +#define SUBSAMPLE_INTERPOLATE 0x1 +#define BLACKMAN_HARRIS 0x2 +#define INCLUDE_LOWPASS 0x4 + +typedef struct { + int numChannels, numSamples, numFilters, numTaps, inputIndex, flags; + float *tempFilter, outputOffset; + float **buffers, **filters; +} Resample; + +typedef struct { + unsigned int input_used, output_generated; +} ResampleResult; + +#ifdef __cplusplus +extern "C" { +#endif + +Resample *resampleInit(int numChannels, int numTaps, int numFilters, float lowpassRatio, int flags); +ResampleResult resampleProcess(Resample *cxt, const float *const *input, int numInputFrames, float *const *output, + int numOutputFrames, float ratio); +ResampleResult resampleProcessInterleaved(Resample *cxt, const float *input, int numInputFrames, float *output, + int numOutputFrames, float ratio); +unsigned int resampleGetRequiredSamples(Resample *cxt, int numOutputFrames, float ratio); +unsigned int resampleGetExpectedOutput(Resample *cxt, int numInputFrames, float ratio); +void resampleAdvancePosition(Resample *cxt, float delta); +float resampleGetPosition(Resample *cxt); +void resampleReset(Resample *cxt); +void resampleFree(Resample *cxt); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/esphome/components/satellite1/media_player/wav_decoder.cpp b/esphome/components/satellite1/media_player/wav_decoder.cpp new file mode 100644 index 00000000..c3e97f0e --- /dev/null +++ b/esphome/components/satellite1/media_player/wav_decoder.cpp @@ -0,0 +1,127 @@ +#ifdef USE_ESP_IDF +#include "wav_decoder.h" + +namespace wav_decoder { + +WAVDecoderResult WAVDecoder::next() { + this->bytes_to_skip_ = 0; + + switch (this->state_) { + case WAV_DECODER_BEFORE_RIFF: { + this->chunk_name_ = std::string((const char *) *this->buffer_, 4); + if (this->chunk_name_ != "RIFF") { + return WAV_DECODER_ERROR_NO_RIFF; + } + + this->chunk_bytes_left_ = *((uint32_t *) (*this->buffer_ + 4)); + if ((this->chunk_bytes_left_ % 2) != 0) { + // Pad byte + this->chunk_bytes_left_++; + } + + // WAVE sub-chunk header should follow + this->state_ = WAV_DECODER_BEFORE_WAVE; + this->bytes_needed_ = 4; // WAVE + break; + } + + case WAV_DECODER_BEFORE_WAVE: { + this->chunk_name_ = std::string((const char *) *this->buffer_, 4); + if (this->chunk_name_ != "WAVE") { + return WAV_DECODER_ERROR_NO_WAVE; + } + + // Next chunk header + this->state_ = WAV_DECODER_BEFORE_FMT; + this->bytes_needed_ = 8; // chunk name + size + break; + } + + case WAV_DECODER_BEFORE_FMT: { + this->chunk_name_ = std::string((const char *) *this->buffer_, 4); + this->chunk_bytes_left_ = *((uint32_t *) (*this->buffer_ + 4)); + if ((this->chunk_bytes_left_ % 2) != 0) { + // Pad byte + this->chunk_bytes_left_++; + } + + if (this->chunk_name_ == "fmt ") { + // Read rest of fmt chunk + this->state_ = WAV_DECODER_IN_FMT; + this->bytes_needed_ = this->chunk_bytes_left_; + } else { + // Skip over chunk + // this->state_ = WAV_DECODER_BEFORE_FMT_SKIP_CHUNK; + this->bytes_to_skip_ = this->chunk_bytes_left_; + this->bytes_needed_ = 8; + } + break; + } + + // case WAV_DECODER_BEFORE_FMT_SKIP_CHUNK: { + // // Next chunk header + // this->state_ = WAV_DECODER_BEFORE_FMT; + // this->bytes_needed_ = 8; // chunk name + size + // break; + // } + + case WAV_DECODER_IN_FMT: { + /** + * audio format (uint16_t) + * number of channels (uint16_t) + * sample rate (uint32_t) + * bytes per second (uint32_t) + * block align (uint16_t) + * bits per sample (uint16_t) + * [rest of format chunk] + */ + this->num_channels_ = *((uint16_t *) (*this->buffer_ + 2)); + this->sample_rate_ = *((uint32_t *) (*this->buffer_ + 4)); + this->bits_per_sample_ = *((uint16_t *) (*this->buffer_ + 14)); + + // Next chunk + this->state_ = WAV_DECODER_BEFORE_DATA; + this->bytes_needed_ = 8; // chunk name + size + break; + } + + case WAV_DECODER_BEFORE_DATA: { + this->chunk_name_ = std::string((const char *) *this->buffer_, 4); + this->chunk_bytes_left_ = *((uint32_t *) (*this->buffer_ + 4)); + if ((this->chunk_bytes_left_ % 2) != 0) { + // Pad byte + this->chunk_bytes_left_++; + } + + if (this->chunk_name_ == "data") { + // Complete + this->state_ = WAV_DECODER_IN_DATA; + this->bytes_needed_ = 0; + return WAV_DECODER_SUCCESS_IN_DATA; + } + + // Skip over chunk + // this->state_ = WAV_DECODER_BEFORE_DATA_SKIP_CHUNK; + this->bytes_to_skip_ = this->chunk_bytes_left_; + this->bytes_needed_ = 8; + break; + } + + // case WAV_DECODER_BEFORE_DATA_SKIP_CHUNK: { + // // Next chunk header + // this->state_ = WAV_DECODER_BEFORE_DATA; + // this->bytes_needed_ = 8; // chunk name + size + // break; + // } + + case WAV_DECODER_IN_DATA: { + return WAV_DECODER_SUCCESS_IN_DATA; + break; + } + } + + return WAV_DECODER_SUCCESS_NEXT; +} + +} // namespace wav_decoder +#endif \ No newline at end of file diff --git a/esphome/components/satellite1/media_player/wav_decoder.h b/esphome/components/satellite1/media_player/wav_decoder.h new file mode 100644 index 00000000..871f70ff --- /dev/null +++ b/esphome/components/satellite1/media_player/wav_decoder.h @@ -0,0 +1,104 @@ +#ifdef USE_ESP_IDF + +// Very basic WAV file decoder that parses format information and gets to the +// data portion of the file. +// Skips over extraneous chunks like LIST and INFO. + +#ifndef WAV_DECODER_H_ +#define WAV_DECODER_H_ + +#include +#include + +/* WAV header: + * 'RIFF' (4 bytes, ASCII) + * RIFF chunk size (uint32_t) + * 'WAVE' (4 bytes, ASCII) + * (optional RIFF chunks) + * 'fmt ' (4 bytes, ASCII) + * format chunk size (uint32_t) + * audio format (uint16_t, PCM = 1) + * number of channels (uint16_t) + * sample rate (uint32_t) + * bytes per second (uint32_t) + * block align (uint16_t) + * bits per sample (uint16_t) + * [rest of format chunk] + * (optional RIFF chunks) + * 'data' (4 bytes, ASCII) + * data chunks size (uint32_t) + * [rest of data chunk] + * (optional RIFF chunks) + * */ + +namespace wav_decoder { + +const std::size_t min_buffer_size = 24; + +enum WAVDecoderState { + + WAV_DECODER_BEFORE_RIFF = 0, + WAV_DECODER_BEFORE_WAVE = 1, + WAV_DECODER_BEFORE_FMT = 2, + WAV_DECODER_IN_FMT = 3, + WAV_DECODER_BEFORE_DATA = 4, + WAV_DECODER_IN_DATA = 5, + +}; + +enum WAVDecoderResult { + WAV_DECODER_SUCCESS_NEXT = 0, + WAV_DECODER_SUCCESS_IN_DATA = 1, + WAV_DECODER_ERROR_NO_RIFF = 2, + WAV_DECODER_ERROR_NO_WAVE = 3, +}; + +class WAVDecoder { + public: + WAVDecoder(uint8_t **buffer) : buffer_(buffer) {}; + ~WAVDecoder() {}; + + WAVDecoderState state() { return this->state_; } + std::size_t bytes_to_skip() { return this->bytes_to_skip_; } + std::size_t bytes_needed() { return this->bytes_needed_; } + std::string chunk_name() { return this->chunk_name_; } + std::size_t chunk_bytes_left() { return this->chunk_bytes_left_; } + uint32_t sample_rate() { return this->sample_rate_; } + uint16_t num_channels() { return this->num_channels_; } + uint16_t bits_per_sample() { return this->bits_per_sample_; } + + // Advance decoding: + // 1. Check bytes_to_skip() first, and skip that many bytes. + // 2. Read exactly bytes_needed() into the start of the buffer. + // 3. Run next() and loop to 1 until the result is + // WAV_DECODER_SUCCESS_IN_DATA. + // 4. Use chunk_bytes_left() to read the data samples. + WAVDecoderResult next(); + + void reset() { + this->state_ = WAV_DECODER_BEFORE_RIFF; + this->bytes_to_skip_ = 0; + this->chunk_name_ = ""; + this->chunk_bytes_left_ = 0; + + this->sample_rate_ = 0; + this->num_channels_ = 0; + this->bits_per_sample_ = 0; + } + + protected: + uint8_t **buffer_; + WAVDecoderState state_ = WAV_DECODER_BEFORE_RIFF; + std::size_t bytes_needed_ = 8; // chunk name + size + std::size_t bytes_to_skip_ = 0; + std::string chunk_name_; + std::size_t chunk_bytes_left_ = 0; + + uint32_t sample_rate_ = 0; + uint16_t num_channels_ = 0; + uint16_t bits_per_sample_ = 0; +}; +} // namespace wav_decoder + +#endif // WAV_DECODER_H_ +#endif \ No newline at end of file diff --git a/esphome/components/satellite1/microphone/__init__.py b/esphome/components/satellite1/microphone/__init__.py new file mode 100644 index 00000000..97cf42e2 --- /dev/null +++ b/esphome/components/satellite1/microphone/__init__.py @@ -0,0 +1,125 @@ +import esphome.config_validation as cv +import esphome.codegen as cg + +from esphome import pins +from esphome.const import CONF_ID, CONF_NUMBER +from esphome.components import microphone, esp32 +from esphome.components.adc import ESP32_VARIANT_ADC1_PIN_TO_CHANNEL, validate_adc_pin + +from esphome.components.i2s_audio.i2s_settings import ( + BITS_PER_SAMPLE, + CONF_CLK_MODE, + I2S_CLK_MODES, + EXTERNAL_CLK, + CONF_CHANNEL, + CONF_FIXED_SETTINGS, + CHANNEL_FORMAT, + _validate_bits, +) + +from esphome.components.i2s_audio import ( + I2SAudioComponent, + I2SReader, + CONF_BITS_PER_SAMPLE, + CONF_I2S_AUDIO_ID, + CONF_I2S_DIN_PIN, + register_i2s_reader +) + +CODEOWNERS = ["@gnumpi"] +DEPENDENCIES = ["i2s_audio"] + +CONF_ADC_PIN = "adc_pin" +CONF_AMPLIFY_SHIFT = "amplify_shift" +CONF_CHANNEL_0 = "channel_0" +CONF_CHANNEL_1 = "channel_1" +CONF_PDM = "pdm" +CONF_SAMPLE_RATE = "sample_rate" +CONF_USE_APLL = "use_apll" + + +nabu_microphone_ns = cg.esphome_ns.namespace("nabu_microphone") + +NabuMicrophone = nabu_microphone_ns.class_("NabuMicrophone", I2SReader, cg.Component) +NabuMicrophoneChannel = nabu_microphone_ns.class_( + "NabuMicrophoneChannel", microphone.Microphone, cg.Component +) + +i2s_channel_fmt_t = cg.global_ns.enum("i2s_channel_fmt_t") +CHANNELS = { + "left": i2s_channel_fmt_t.I2S_CHANNEL_FMT_ONLY_LEFT, + "right": i2s_channel_fmt_t.I2S_CHANNEL_FMT_ONLY_RIGHT, +} + + + +MICROPHONE_CHANNEL_SCHEMA = microphone.MICROPHONE_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(NabuMicrophoneChannel), + cv.Optional(CONF_AMPLIFY_SHIFT, default=0): cv.All( + cv.uint8_t, cv.Range(min=0, max=8) + ), + } +) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(NabuMicrophone), + cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent), + cv.Optional(CONF_SAMPLE_RATE, default=48000): cv.int_range(min=1), + cv.Optional(CONF_BITS_PER_SAMPLE, default="32bit"): cv.All( + _validate_bits, cv.enum(BITS_PER_SAMPLE) + ), + cv.Optional(CONF_CLK_MODE, default=EXTERNAL_CLK): cv.enum(I2S_CLK_MODES), + cv.Optional(CONF_CHANNEL, default="right_left"): cv.enum(CHANNEL_FORMAT), + cv.Optional(CONF_USE_APLL, default=False): cv.boolean, + cv.Optional(CONF_CHANNEL_0): MICROPHONE_CHANNEL_SCHEMA, + cv.Optional(CONF_CHANNEL_1): MICROPHONE_CHANNEL_SCHEMA, + + cv.Required(CONF_I2S_DIN_PIN): pins.internal_gpio_input_pin_number, + cv.Required(CONF_PDM): cv.boolean, + cv.Optional(CONF_FIXED_SETTINGS, default=True): cv.boolean, + + } +).extend(cv.COMPONENT_SCHEMA) + + +def _supported_satellite1_settings(config): + if config[CONF_PDM] : + raise cv.Invalid("PDM is not supported for the Satellite1 microphone integration.") + if config[CONF_BITS_PER_SAMPLE] != 32: + raise cv.Invalid("I2S needs to be set to 32bit for the satellite1 microphone integration.") + if config[CONF_SAMPLE_RATE] != 48000: + raise cv.Invalid("I2S needs to be set to 48kHz, downsampling to 16kHz is hard coded.") + + + +FINAL_VALIDATE_SCHEMA = _supported_satellite1_settings + + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + await cg.register_parented(var, config[CONF_I2S_AUDIO_ID]) + + if channel_0_config := config.get(CONF_CHANNEL_0): + channel_0 = cg.new_Pvariable(channel_0_config[CONF_ID]) + await cg.register_component(channel_0, channel_0_config) + await cg.register_parented(channel_0, config[CONF_ID]) + await microphone.register_microphone(channel_0, channel_0_config) + cg.add(var.set_channel_0(channel_0)) + cg.add(channel_0.set_amplify_shift(channel_0_config[CONF_AMPLIFY_SHIFT])) + + if channel_1_config := config.get(CONF_CHANNEL_1): + channel_1 = cg.new_Pvariable(channel_1_config[CONF_ID]) + await cg.register_component(channel_1, channel_1_config) + await cg.register_parented(channel_1, config[CONF_ID]) + await microphone.register_microphone(channel_1, channel_1_config) + cg.add(var.set_channel_1(channel_1)) + cg.add(channel_1.set_amplify_shift(channel_1_config[CONF_AMPLIFY_SHIFT])) + + await register_i2s_reader(var, config) + + cg.add_define("USE_OTA_STATE_CALLBACK") diff --git a/esphome/components/satellite1/microphone/sat1_microphone.cpp b/esphome/components/satellite1/microphone/sat1_microphone.cpp new file mode 100644 index 00000000..44f8b73a --- /dev/null +++ b/esphome/components/satellite1/microphone/sat1_microphone.cpp @@ -0,0 +1,357 @@ +#include "sat1_microphone.h" + +#ifdef USE_ESP32 + +#include + +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" +#include "esphome/core/ring_buffer.h" + +#ifdef USE_OTA +#include "esphome/components/ota/ota_backend.h" +#endif + +namespace esphome { +namespace nabu_microphone { + +static const size_t RING_BUFFER_LENGTH = 64; // Measured in milliseconds +static const size_t QUEUE_LENGTH = 10; + +static const size_t NUMBER_OF_CHANNELS = 2; +static const size_t DMA_BUFFER_SIZE = 512; +static const size_t DMA_BUFFERS_COUNT = 6; +static const size_t FRAMES_IN_ALL_DMA_BUFFERS = DMA_BUFFER_SIZE * DMA_BUFFERS_COUNT; +static const size_t SAMPLES_IN_ALL_DMA_BUFFERS = FRAMES_IN_ALL_DMA_BUFFERS * NUMBER_OF_CHANNELS; + +static const size_t TASK_DELAY_MS = 10; + +// TODO: +// - Determine optimal buffer sizes (dma included) +// - Determine appropriate timeout durations for FreeRTOS operations +// - Test if stopping the microphone behaves properly + +// Notes on things taken out/removed: +// - Doesn't properly handle 16 bit samples +// - Removed the watch_ function and handling any callbacks +// - Channels are fixed to left and right for the XMOS chip + +static const char *const TAG = "i2s_audio.microphone"; + +enum TaskNotificationBits : uint32_t { + COMMAND_START = (1 << 0), // Starts the main task purpose + COMMAND_STOP = (1 << 1), // stops the main task +}; + +void NabuMicrophoneChannel::setup() { + const size_t ring_buffer_size = RING_BUFFER_LENGTH * this->parent_->get_sample_rate() / 1000 * sizeof(int16_t); + this->ring_buffer_ = RingBuffer::create(ring_buffer_size); + if (this->ring_buffer_ == nullptr) { + ESP_LOGE(TAG, "Could not allocate ring buffer"); + this->mark_failed(); + return; + } +} + +void NabuMicrophoneChannel::loop() { + if (this->parent_->is_running()) { + if (this->is_muted_) { + if (this->requested_stop_) { + // The microphone was muted when stopping was requested + this->state_ = microphone::STATE_STOPPED; + } else { + this->state_ = microphone::STATE_MUTED; + } + } else { + this->state_ = microphone::STATE_RUNNING; + } + } else { + this->state_ = microphone::STATE_STOPPED; + } +} + +void NabuMicrophone::setup() { + ESP_LOGCONFIG(TAG, "Setting up I2S Audio Microphone..."); + if (this->pdm_) { + if (this->parent_->get_port() != I2S_NUM_0) { + ESP_LOGE(TAG, "PDM only works on I2S0!"); + this->mark_failed(); + return; + } + } + + this->event_queue_ = xQueueCreate(QUEUE_LENGTH, sizeof(TaskEvent)); + +#ifdef USE_OTA + ota::get_global_ota_callback()->add_on_state_callback( + [this](ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) { + if (state == ota::OTA_STARTED) { + if (this->read_task_handle_ != nullptr) { + vTaskSuspend(this->read_task_handle_); + } + } else if (state == ota::OTA_ERROR) { + if (this->read_task_handle_ != nullptr) { + vTaskResume(this->read_task_handle_); + } + } + }); +#endif +} + +void NabuMicrophone::mute() { + if (this->channel_0_ != nullptr) { + this->channel_0_->set_mute_state(true); + } + if (this->channel_1_ != nullptr) { + this->channel_1_->set_mute_state(true); + } +} + +void NabuMicrophone::unmute() { + if (this->channel_0_ != nullptr) { + this->channel_0_->set_mute_state(false); + } + if (this->channel_1_ != nullptr) { + this->channel_1_->set_mute_state(false); + } +} + +esp_err_t NabuMicrophone::start_i2s_driver_() { + if (!this->claim_i2s_access()) { + return ESP_ERR_INVALID_STATE; + } + + i2s_driver_config_t config = this->get_i2s_cfg(); + if(!this->install_i2s_driver(config)) + { + this->release_i2s_access(); + return ESP_ERR_INVALID_STATE; + } + + return ESP_OK; +} + +void NabuMicrophone::read_task_(void *params) { + NabuMicrophone *this_microphone = (NabuMicrophone *) params; + TaskEvent event; + esp_err_t err; + + while (true) { + uint32_t notification_bits = 0; + xTaskNotifyWait(ULONG_MAX, // clear all bits at start of wait + ULONG_MAX, // clear all bits after waiting + ¬ification_bits, // notifcation value after wait is finished + portMAX_DELAY); // how long to wait + + if (notification_bits & TaskNotificationBits::COMMAND_START) { + event.type = TaskEventType::STARTING; + xQueueSend(this_microphone->event_queue_, &event, portMAX_DELAY); + + if ((this_microphone->channel_0_ != nullptr) && this_microphone->channel_0_->is_failed()) { + event.type = TaskEventType::WARNING; + event.err = ESP_ERR_INVALID_STATE; + xQueueSend(this_microphone->event_queue_, &event, portMAX_DELAY); + continue; + } + + if ((this_microphone->channel_1_ != nullptr) && this_microphone->channel_1_->is_failed()) { + event.type = TaskEventType::WARNING; + event.err = ESP_ERR_INVALID_STATE; + xQueueSend(this_microphone->event_queue_, &event, portMAX_DELAY); + continue; + } + + // Note, if we have 16 bit samples incoming, this requires modification + ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + int32_t *buffer = allocator.allocate(SAMPLES_IN_ALL_DMA_BUFFERS * 3); + + std::vector> channel_0_samples; + std::vector> channel_1_samples; + + size_t channel_0_reserved_samples = 0; + size_t channel_1_reserved_samples = 0; + + if (this_microphone->channel_0_ != nullptr) { + channel_0_reserved_samples = FRAMES_IN_ALL_DMA_BUFFERS; + channel_0_samples.reserve(channel_0_reserved_samples); + } + + if (this_microphone->channel_1_ != nullptr) { + channel_1_reserved_samples = FRAMES_IN_ALL_DMA_BUFFERS; + channel_1_samples.reserve(channel_1_reserved_samples); + } + + if ((buffer == nullptr) || (channel_0_samples.capacity() < channel_0_reserved_samples) || + (channel_1_samples.capacity() < channel_1_reserved_samples)) { + event.type = TaskEventType::WARNING; + event.err = ESP_ERR_NO_MEM; + xQueueSend(this_microphone->event_queue_, &event, portMAX_DELAY); + } else { + err = this_microphone->start_i2s_driver_(); + if (err != ESP_OK) { + event.type = TaskEventType::WARNING; + event.err = err; + xQueueSend(this_microphone->event_queue_, &event, portMAX_DELAY); + } else { + // TODO: Is this the ideal spot to reset the ring buffers? + if (this_microphone->channel_0_ != nullptr) + this_microphone->channel_0_->get_ring_buffer()->reset(); + if (this_microphone->channel_1_ != nullptr) + this_microphone->channel_1_->get_ring_buffer()->reset(); + + event.type = TaskEventType::STARTED; + xQueueSend(this_microphone->event_queue_, &event, portMAX_DELAY); + + while (true) { + notification_bits = ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(0)); + if (notification_bits & TaskNotificationBits::COMMAND_STOP) { + break; + } + + size_t bytes_read; + esp_err_t err = + i2s_read(this_microphone->parent_->get_port(), buffer, SAMPLES_IN_ALL_DMA_BUFFERS * sizeof(int32_t) * 3, + &bytes_read, pdMS_TO_TICKS(TASK_DELAY_MS)); + if (err != ESP_OK) { + event.type = TaskEventType::WARNING; + event.err = err; + xQueueSend(this_microphone->event_queue_, &event, portMAX_DELAY); + } + + if (bytes_read > 0) { + // TODO: Handle 16 bits per sample, currently it won't allow that option at codegen stage + + const size_t samples_read = bytes_read / sizeof(int32_t) / 3; + const size_t frames_read = + samples_read / NUMBER_OF_CHANNELS; // Left and right channel samples combine into 1 frame + + uint8_t channel_0_shift = 16; + if (this_microphone->channel_0_ != nullptr) { + channel_0_shift -= this_microphone->channel_0_->get_amplify_shift(); + } + uint8_t channel_1_shift = 16; + if (this_microphone->channel_1_ != nullptr) { + channel_1_shift -= this_microphone->channel_1_->get_amplify_shift(); + } + + for (size_t i = 0; i < frames_read; i++) { + int32_t channel_0_sample = 0; + if ((this_microphone->channel_0_ != nullptr) && (!this_microphone->channel_0_->get_mute_state())) { + channel_0_sample = buffer[ 3 * NUMBER_OF_CHANNELS * i] >> channel_0_shift; + channel_0_samples[i] = (int16_t) clamp(channel_0_sample, INT16_MIN, INT16_MAX); + } + + int32_t channel_1_sample = 0; + if ((this_microphone->channel_1_ != nullptr) && (!this_microphone->channel_1_->get_mute_state())) { + channel_1_sample = buffer[3 * NUMBER_OF_CHANNELS * i + 1] >> channel_1_shift; + channel_1_samples[i] = (int16_t) clamp(channel_1_sample, INT16_MIN, INT16_MAX); + } + } + + size_t bytes_to_write = frames_read * sizeof(int16_t); + + if (this_microphone->channel_0_ != nullptr) { + this_microphone->channel_0_->get_ring_buffer()->write((void *) channel_0_samples.data(), + bytes_to_write); + } + if (this_microphone->channel_1_ != nullptr) { + this_microphone->channel_1_->get_ring_buffer()->write((void *) channel_1_samples.data(), + bytes_to_write); + } + } + + event.type = TaskEventType::RUNNING; + xQueueSend(this_microphone->event_queue_, &event, 0); + } + + event.type = TaskEventType::STOPPING; + xQueueSend(this_microphone->event_queue_, &event, portMAX_DELAY); + + allocator.deallocate(buffer, SAMPLES_IN_ALL_DMA_BUFFERS); + + this_microphone->uninstall_i2s_driver(); + this_microphone->release_i2s_access(); + + + event.type = TaskEventType::STOPPED; + xQueueSend(this_microphone->event_queue_, &event, portMAX_DELAY); + } + } + } + event.type = TaskEventType::STOPPED; + event.err = ESP_OK; + xQueueSend(this_microphone->event_queue_, &event, portMAX_DELAY); + } +} + +void NabuMicrophone::start() { + if (this->is_failed()) + return; + if ((this->state_ == microphone::STATE_STARTING) || (this->state_ == microphone::STATE_RUNNING)) + return; + + if (this->read_task_handle_ == nullptr) { + xTaskCreate(NabuMicrophone::read_task_, "microphone_task", 3584, (void *) this, 23, &this->read_task_handle_); + } + + // TODO: Should we overwrite? If stop and start are called in quick succession, what behavior do we want + xTaskNotify(this->read_task_handle_, TaskNotificationBits::COMMAND_START, eSetValueWithoutOverwrite); +} + +void NabuMicrophone::stop() { + if (this->state_ == microphone::STATE_STOPPED || this->is_failed()) + return; + + xTaskNotify(this->read_task_handle_, TaskNotificationBits::COMMAND_STOP, eSetValueWithOverwrite); +} + +void NabuMicrophone::loop() { + if ((this->channel_0_ != nullptr) && (this->channel_0_->get_requested_stop()) && (this->channel_1_ != nullptr) && + (this->channel_1_->get_requested_stop())) { + // Both microphone channels have requested a stop + this->stop(); + } + + // Note this->state_ is only modified here based on the status of the task + TaskEvent event; + while (xQueueReceive(this->event_queue_, &event, 0)) { + switch (event.type) { + case TaskEventType::STARTING: + this->state_ = microphone::STATE_STARTING; + ESP_LOGD(TAG, "Starting I2S Audio Microphne"); + break; + case TaskEventType::STARTED: + this->state_ = microphone::STATE_RUNNING; + ESP_LOGD(TAG, "Started I2S Audio Microphone"); + break; + case TaskEventType::RUNNING: + this->state_ = microphone::STATE_RUNNING; + this->status_clear_warning(); + break; + case TaskEventType::MUTED: + this->state_ = microphone::STATE_MUTED; + ESP_LOGD(TAG, "Muted I2S Audio Microphone"); + break; + case TaskEventType::STOPPING: + this->state_ = microphone::STATE_STOPPING; + ESP_LOGD(TAG, "Stopping I2S Audio Microphone"); + break; + case TaskEventType::STOPPED: + this->state_ = microphone::STATE_STOPPED; + ESP_LOGD(TAG, "Stopped I2S Audio Microphone"); + break; + case TaskEventType::WARNING: + ESP_LOGW(TAG, "Error involving I2S: %s", esp_err_to_name(event.err)); + this->status_set_warning(); + break; + case TaskEventType::IDLE: + break; + } + } +} + +} // namespace nabu_microphone +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/satellite1/microphone/sat1_microphone.h b/esphome/components/satellite1/microphone/sat1_microphone.h new file mode 100644 index 00000000..c24bd008 --- /dev/null +++ b/esphome/components/satellite1/microphone/sat1_microphone.h @@ -0,0 +1,117 @@ +#pragma once + +#ifdef USE_ESP32 + +#include +#include + +#include "esphome/components/i2s_audio/i2s_audio.h" +#include "esphome/components/microphone/microphone.h" +#include "esphome/core/component.h" +#include "esphome/core/ring_buffer.h" + +namespace esphome { +namespace nabu_microphone { + +enum class TaskEventType : uint8_t { + STARTING = 0, + STARTED, + RUNNING, + IDLE, + STOPPING, + STOPPED, + MUTED, + WARNING = 255, +}; + +struct TaskEvent { + TaskEventType type; + esp_err_t err; +}; + +class NabuMicrophoneChannel; + +class NabuMicrophone : public i2s_audio::I2SReader, public Component { + public: + void setup() override; + void dump_config() override {this->dump_i2s_settings();} + void start(); + void stop(); + + void loop() override; + + void mute(); + void unmute(); + + void set_channel_0(NabuMicrophoneChannel *microphone) { this->channel_0_ = microphone; } + void set_channel_1(NabuMicrophoneChannel *microphone) { this->channel_1_ = microphone; } + + NabuMicrophoneChannel *get_channel_0() { return this->channel_0_; } + NabuMicrophoneChannel *get_channel_1() { return this->channel_1_; } + + bool is_running() { return this->state_ == microphone::STATE_RUNNING; } + uint32_t get_sample_rate() { return this->sample_rate_; } + + protected: + esp_err_t start_i2s_driver_(); + + microphone::State state_{microphone::STATE_STOPPED}; + + static void read_task_(void *params); + + TaskHandle_t read_task_handle_{nullptr}; + QueueHandle_t event_queue_; + + NabuMicrophoneChannel *channel_0_{nullptr}; + NabuMicrophoneChannel *channel_1_{nullptr}; +}; + +class NabuMicrophoneChannel : public microphone::Microphone, public Component { + public: + void setup() override; + + void start() override { + this->parent_->start(); + this->is_muted_ = false; + this->requested_stop_ = false; + } + + void set_parent(NabuMicrophone *nabu_microphone) { this->parent_ = nabu_microphone; } + + void stop() override { + this->requested_stop_ = true; + this->is_muted_ = true; // Mute until it is actually stopped + }; + + void loop() override; + + void set_mute_state(bool mute_state) override { this->is_muted_ = mute_state; } + bool get_mute_state() { return this->is_muted_; } + + // void set_requested_stop() { this->requested_stop_ = true; } + bool get_requested_stop() { return this->requested_stop_; } + + size_t read(int16_t *buf, size_t len, TickType_t ticks_to_wait = 0) override { + return this->ring_buffer_->read((void *) buf, len, ticks_to_wait); + }; + size_t read(int16_t *buf, size_t len) override { return this->ring_buffer_->read((void *) buf, len); }; + void reset() override { this->ring_buffer_->reset(); } + + RingBuffer *get_ring_buffer() { return this->ring_buffer_.get(); } + + void set_amplify_shift(uint8_t amplify_shift) { this->amplify_shift_ = amplify_shift; } + uint8_t get_amplify_shift() { return this->amplify_shift_; } + + protected: + NabuMicrophone *parent_; + std::unique_ptr ring_buffer_; + + uint8_t amplify_shift_; + bool is_muted_; + bool requested_stop_; +}; + +} // namespace nabu_microphone +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/satellite1/ota/__init__.py b/esphome/components/satellite1/ota/__init__.py new file mode 100644 index 00000000..585141f8 --- /dev/null +++ b/esphome/components/satellite1/ota/__init__.py @@ -0,0 +1,97 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.const import ( + CONF_ID, + CONF_URL, + +) +from esphome.components.http_request import ( + CONF_HTTP_REQUEST_ID, + HttpRequestComponent, +) + +from esphome.components.ota import BASE_OTA_SCHEMA, ota_to_code, OTAComponent +from esphome.core import coroutine_with_priority + +from .. import ( + CONF_SATELLITE1, + satellite1_ns, + Satellite1, + Satellite1SPIService +) + + +CODEOWNERS = ["@gnumpi"] + +AUTO_LOAD = ["md5"] +DEPENDENCIES = ["network", "http_request", "satellite1"] + +CONF_MD5 = "md5" +CONF_MD5_URL = "md5_url" + +SatelliteFlasher = satellite1_ns.class_("SatelliteFlasher", OTAComponent, Satellite1SPIService) +SatelliteFlasherAction = satellite1_ns.class_( + "SatelliteFlasherAction", automation.Action +) + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(SatelliteFlasher), + cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent), + cv.GenerateID(CONF_SATELLITE1): cv.use_id(Satellite1) + } + ) + .extend(BASE_OTA_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + + +@coroutine_with_priority(52.0) +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_parented(var, config[CONF_SATELLITE1]) + await ota_to_code(var, config) + await cg.register_component(var, config) + http_comp = await cg.get_variable(config[CONF_HTTP_REQUEST_ID]) + cg.add( var.set_http_request_component(http_comp) ) + + + +OTA_SATELLITE_FLASH_ACTION_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.use_id(SatelliteFlasher), + cv.Optional(CONF_MD5_URL): cv.templatable(cv.url), + cv.Optional(CONF_MD5): cv.templatable( + cv.All(cv.string, cv.Length(min=32, max=32)) + ), + cv.Required(CONF_URL): cv.templatable(cv.url), + } + ), + cv.has_exactly_one_key(CONF_MD5, CONF_MD5_URL), +) + + +@automation.register_action( + "ota.satellite1.flash", + SatelliteFlasherAction, + OTA_SATELLITE_FLASH_ACTION_SCHEMA, +) +async def ota_voice_kit_action_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + + if md5_url := config.get(CONF_MD5_URL): + template_ = await cg.templatable(md5_url, args, cg.std_string) + cg.add(var.set_md5_url(template_)) + + if md5_str := config.get(CONF_MD5): + template_ = await cg.templatable(md5_str, args, cg.std_string) + cg.add(var.set_md5(template_)) + + template_ = await cg.templatable(config[CONF_URL], args, cg.std_string) + cg.add(var.set_url(template_)) + + return var \ No newline at end of file diff --git a/esphome/components/satellite1/ota/automation.h b/esphome/components/satellite1/ota/automation.h new file mode 100644 index 00000000..4dd9e647 --- /dev/null +++ b/esphome/components/satellite1/ota/automation.h @@ -0,0 +1,34 @@ +#pragma once +#include "flashing.h" + +#include "esphome/core/automation.h" + +namespace esphome { +namespace satellite1 { + +template class SatelliteFlasherAction : public Action { + public: + SatelliteFlasherAction(SatelliteFlasher *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(std::string, md5_url) + TEMPLATABLE_VALUE(std::string, md5) + TEMPLATABLE_VALUE(std::string, url) + + void play(Ts... x) override { + if (this->md5_url_.has_value()) { + this->parent_->set_md5_url(this->md5_url_.value(x...)); + } + if (this->md5_.has_value()) { + this->parent_->set_md5(this->md5_.value(x...)); + } + this->parent_->set_url(this->url_.value(x...)); + + this->parent_->flash(); + // Normally never reached due to reboot + } + + protected: + SatelliteFlasher *parent_; +}; + +} // namespace satellite1 +} // namespace esphome \ No newline at end of file diff --git a/esphome/components/satellite1/ota/flashing.cpp b/esphome/components/satellite1/ota/flashing.cpp new file mode 100644 index 00000000..46d82e89 --- /dev/null +++ b/esphome/components/satellite1/ota/flashing.cpp @@ -0,0 +1,443 @@ +#include "flashing.h" +#include "esphome/core/log.h" + +#include +#include "esphome/components/md5/md5.h" + +namespace esphome { +namespace satellite1 { + +static const char *const TAG = "xmos_flasher"; + +static const size_t FLASH_PAGE_SIZE = 256; +static const size_t FLASH_SECTOR_SIZE = 4096; + +void SatelliteFlasher::setup() { +} + +void SatelliteFlasher::dump_config(){ +} + + +void SatelliteFlasher::dump_flash_info(){ + esph_log_config(TAG, "Satellite1-Flasher:"); + esph_log_config(TAG, " JEDEC-manufacturerID %hhu", this->_manufacturerID); + esph_log_config(TAG, " JEDEC-memoryTypeID %hhu", this->_memoryTypeID); + esph_log_config(TAG, " JEDEC-capacityID %hhu", this->_capacityID); +} + + +bool SatelliteFlasher::init_flasher(){ + ESP_LOGD(TAG, "Setting up XMOS flasher..."); + this->parent_->set_spi_flash_direct_access_mode(true); + this->read_JEDECID_(); + this->dump_flash_info(); + return true; + } + +bool SatelliteFlasher::deinit_flasher(){ + ESP_LOGD(TAG, "Stopping XMOS flasher..."); + this->parent_->set_spi_flash_direct_access_mode(false); + return true; + } + + +void SatelliteFlasher::read_JEDECID_(){ + /* + _beginSPI(JEDECID); { _nextByte(WRITE, 0x9F);} + _chip.manufacturerID = _nextByte(READ); // manufacturer id + _chip.memoryTypeID = _nextByte(READ); // memory type + _chip.capacityID = _nextByte(READ); // capacity + */ + this->enable(); + this->transfer_byte(0x9F); + this->_manufacturerID = this->transfer_byte(0); + this->_memoryTypeID = this->transfer_byte(0); + this->_capacityID = this->transfer_byte(0); + this->disable(); +} + + +bool SatelliteFlasher::http_get_md5_(){ + if (this->md5_url_.empty()) { + return false; + } + + ESP_LOGI(TAG, "Connecting to: %s", this->md5_url_.c_str()); + auto container = this->http_request_->get(this->md5_url_); + if (container == nullptr) { + ESP_LOGE(TAG, "Failed to connect to MD5 URL"); + return false; + } + size_t length = container->content_length; + if (length == 0) { + container->end(); + return false; + } + if (length < MD5_SIZE) { + ESP_LOGE(TAG, "MD5 file must be %u bytes; %u bytes reported by HTTP server. Aborting", MD5_SIZE, length); + container->end(); + return false; + } + + this->md5_expected_.resize(MD5_SIZE); + int read_len = 0; + while (container->get_bytes_read() < MD5_SIZE) { + read_len = container->read((uint8_t *) this->md5_expected_.data(), MD5_SIZE); + App.feed_wdt(); + yield(); + } + container->end(); + + ESP_LOGV(TAG, "Read len: %u, MD5 expected: %u", read_len, MD5_SIZE); + return read_len == MD5_SIZE; +} + +bool SatelliteFlasher::validate_url_(const std::string &url){ + return true; +} + +void SatelliteFlasher::set_url(const std::string &url) { + if (!this->validate_url_(url)) { + this->url_.clear(); // URL was not valid; prevent flashing until it is + return; + } + this->url_ = url; +} + +void SatelliteFlasher::set_md5_url(const std::string &url) { + if (!this->validate_url_(url)) { + this->md5_url_.clear(); // URL was not valid; prevent flashing until it is + return; + } + this->md5_url_ = url; + this->md5_expected_.clear(); // to be retrieved later +} + +void SatelliteFlasher::flash(){ + if (this->url_.empty()) { + ESP_LOGE(TAG, "URL not set; cannot start update"); + return; + } + + ESP_LOGI(TAG, "Starting update..."); +#ifdef USE_OTA_STATE_CALLBACK + this->state_callback_.call(ota::OTA_STARTED, 0.0f, 0); +#endif + + uint8_t ota_status; + if (this->init_flasher() ){ + ota_status = this->write_to_flash_(); + } + else { + ota_status = OTA_CONNECTION_ERROR; + } + + ESP_LOGD(TAG, "flashing return status: %d", ota_status); + + switch (ota_status) { + case ota::OTA_RESPONSE_OK: +#ifdef USE_OTA_STATE_CALLBACK + this->state_callback_.call(ota::OTA_COMPLETED, 100.0f, ota_status); +#endif + // delay(10); + // App.safe_reboot(); + break; + + default: +#ifdef USE_OTA_STATE_CALLBACK + this->state_callback_.call(ota::OTA_ERROR, 0.0f, ota_status); +#endif + this->md5_computed_.clear(); // will be reset at next attempt + this->md5_expected_.clear(); // will be reset at next attempt + break; + } + delay(5); + this->deinit_flasher(); +} + + +bool SatelliteFlasher::wait_while_flash_busy_(uint32_t timeout_ms){ + int32_t timeout_invoke = millis(); + const uint8_t WEL = 2; + const uint8_t BUSY = 1; + + while( (millis() - timeout_invoke) < timeout_ms ){ + this->enable(); + this->transfer_byte(0x05); + uint8_t status = this->transfer_byte(0x00); + this->disable(); + if( (status & BUSY) == 0){ + return true; + } + } + return false; +} + +bool SatelliteFlasher::enable_writing_(){ + //enable writing + this->enable(); + this->transfer_byte(0x06); + this->disable(); + + this->enable(); + this->transfer_byte(0x05); + uint8_t status = this->transfer_byte(0x00); + this->disable(); + const uint8_t WEL = 2; + if( !(status & WEL)){ + return false; + } + return true; +} + +bool SatelliteFlasher::disable_writing_(){ + //disable writing + this->enable(); + this->transfer_byte(0x04); + this->disable(); + return true; +} + +bool SatelliteFlasher::erase_sector_(int sector){ + //erase 4kB sector + assert( FLASH_SECTOR_SIZE == 4096 ); + uint32_t u32 = htole32(sector * FLASH_SECTOR_SIZE); + uint8_t* u8_ptr = (uint8_t*) &u32; + + if( !this->enable_writing_()){ + return false; + } + + this->enable(); + this->transfer_byte(0x20); + this->transfer_byte( *(u8_ptr+2)); + this->transfer_byte( *(u8_ptr+1)); + this->transfer_byte( *(u8_ptr) ); + this->disable(); + + if( !this->wait_while_flash_busy_(200) ){ + ESP_LOGE(TAG, "Erasing timeout" ); + return false; + } + this->disable_writing_(); + return true; +} + +bool SatelliteFlasher::write_page_( uint32_t byte_addr, uint8_t* buffer ){ + if ((byte_addr & (FLASH_PAGE_SIZE - 1)) != 0){ + ESP_LOGE(TAG, "Address needs to be page aligned (%d).", FLASH_PAGE_SIZE); + return false; + } + if( !this->enable_writing_()){ + ESP_LOGE(TAG, "Couldn't enable writing"); + return false; + } + + uint32_t u32 = htole32(byte_addr); + uint8_t* u8_ptr = (uint8_t*) &u32; + this->enable(); + this->transfer_byte(0x02); + this->transfer_byte( *(u8_ptr+2)); + this->transfer_byte( *(u8_ptr+1)); + this->transfer_byte( *(u8_ptr) ); + for(int pos = 0; pos < FLASH_PAGE_SIZE; pos++){ + this->transfer_byte(*(buffer + pos)); + } + this->disable(); + + if( !this->wait_while_flash_busy_(15) ){ + ESP_LOGE(TAG, "Writing page timeout"); + return false; + } + this->disable_writing_(); + return true; +} + +bool SatelliteFlasher::read_page_( uint32_t byte_addr, uint8_t* buffer ){ + if ((byte_addr & (FLASH_PAGE_SIZE - 1)) != 0){ + return false; + } + uint32_t u32 = htole32(byte_addr); + uint8_t* u8_ptr = (uint8_t*) &u32; + this->enable(); + this->transfer_byte(0x0B); + this->transfer_byte( *(u8_ptr+2)); + this->transfer_byte( *(u8_ptr+1)); + this->transfer_byte( *(u8_ptr) ); + this->transfer_byte(0x00); + for(int pos=0; pos < FLASH_PAGE_SIZE; pos++){ + *(buffer + pos) = this->transfer_byte(0x00); + } + this->disable(); + return true; +} + + + +uint8_t SatelliteFlasher::write_to_flash_() { + uint32_t last_progress = 0; + uint32_t update_start_time = millis(); + md5::MD5Digest md5_receive; + std::unique_ptr md5_receive_str(new char[33]); + + if (this->md5_expected_.empty() && !this->http_get_md5_()) { + return OTA_MD5_INVALID; + } + + ESP_LOGD(TAG, "MD5 expected: %s", this->md5_expected_.c_str()); + + auto url_with_auth = this->url_; + if (url_with_auth.empty()) { + return OTA_BAD_URL; + } + + // we will compute MD5 on the fly for verification + md5_receive.init(); + ESP_LOGV(TAG, "MD5Digest initialized"); + + ESP_LOGVV(TAG, "url_with_auth: %s", url_with_auth.c_str()); + ESP_LOGI(TAG, "Connecting to: %s", this->url_.c_str()); + + auto container = this->http_request_->get(url_with_auth); + + if (container == nullptr) { + return OTA_CONNECTION_ERROR; + } + + ESP_LOGV(TAG, "OTA begin"); + + size_t size_in_pages = ((container->content_length + FLASH_PAGE_SIZE - 1) / FLASH_PAGE_SIZE); + size_t size_in_sectors = ((size_in_pages * FLASH_PAGE_SIZE + FLASH_SECTOR_SIZE - 1) / FLASH_SECTOR_SIZE); + + + for( int sector = 0; sector < size_in_sectors; sector++ ){ + if( !this->erase_sector_(sector) ){ + ESP_LOGE(TAG, "Error while erasing sector %d", sector ); + return OTA_INIT_FLASH_ERROR; + } + } + + uint8_t dnload_req[HTTP_RECV_BUFFER]; + uint8_t cmp_buf[HTTP_RECV_BUFFER]; + + this->read_page_(0, &cmp_buf[0] ); + for(int pos=0; pos < 10 ; pos++){ + ESP_LOGD(TAG, "%.2x", cmp_buf[pos] ); + } + + size_t page_pos = 0; + while (container->get_bytes_read() < container->content_length) { + + // read a maximum of chunk_size bytes into buf. (real read size returned) + int bufsize = container->read(&dnload_req[0], HTTP_RECV_BUFFER); + ESP_LOGVV(TAG, "bytes_read_ = %u, body_length_ = %u, bufsize = %i", container->get_bytes_read(), + container->content_length, bufsize); + + if (bufsize < 0) { + ESP_LOGE(TAG, "Stream closed"); + this->cleanup_(container); + return OTA_CONNECTION_ERROR; + } else if (bufsize == FLASH_PAGE_SIZE) { + md5_receive.add(&dnload_req[0], bufsize); + + //this->read_page_(page_pos, &dbg_buf[0] ); + + if( !this->write_page_(page_pos, &dnload_req[0] )){ + ESP_LOGE(TAG, "writing page error"); + } + + this->read_page_(page_pos, &cmp_buf[0] ); + + if (memcmp(&dnload_req[0], &cmp_buf[0], FLASH_PAGE_SIZE) != 0){ + //give it a second try + /* + for(int pos=0; pos < FLASH_PAGE_SIZE ; pos++){ + if( cmp_buf[pos] != dnload_req[pos] ){ + ESP_LOGD(TAG, "%d: %.2x - %.2x ", pos, cmp_buf[pos], dnload_req[pos] ); + } + } + */ + if( !this->write_page_(page_pos, &dnload_req[0] )){ + ESP_LOGE(TAG, "writing page error"); + } + + this->read_page_(page_pos, &cmp_buf[0] ); + if (memcmp(&dnload_req[0], &cmp_buf[0], FLASH_PAGE_SIZE) != 0){ + ESP_LOGE(TAG, "Read page mismatch, page addr: %d", page_pos ); + return OTA_INIT_FLASH_ERROR; + } + } + page_pos += FLASH_PAGE_SIZE; + + } else if (bufsize > 0 && bufsize < HTTP_RECV_BUFFER) { + // add read bytes to MD5 + if ( (container->get_bytes_read() != container->content_length) ){ + return OTA_CONNECTION_ERROR; + } + md5_receive.add(&dnload_req[0], bufsize); + memset(&dnload_req[bufsize], 0, HTTP_RECV_BUFFER - bufsize); + this->write_page_(page_pos, &dnload_req[0] ); + this->read_page_(page_pos, &cmp_buf[0] ); + if (memcmp(&dnload_req[0], &cmp_buf[0], FLASH_PAGE_SIZE) != 0 ){ + return OTA_INIT_FLASH_ERROR; + } + page_pos += FLASH_PAGE_SIZE; + } + + uint32_t now = millis(); + if ((now - last_progress > 1000) or (container->get_bytes_read() == container->content_length)) { + last_progress = now; + float percentage = container->get_bytes_read() * 100.0f / container->content_length; + ESP_LOGD(TAG, "Progress: %0.1f%%", percentage); +#ifdef USE_OTA_STATE_CALLBACK + this->state_callback_.call(ota::OTA_IN_PROGRESS, percentage, 0); +#endif + } + } // while + + + container->end(); + + ESP_LOGI(TAG, "Done in %.0f seconds", float(millis() - update_start_time) / 1000); + + // verify MD5 is as expected and act accordingly + md5_receive.calculate(); + md5_receive.get_hex(md5_receive_str.get()); + this->md5_computed_ = md5_receive_str.get(); + if (strncmp(this->md5_computed_.c_str(), this->md5_expected_.c_str(), MD5_SIZE) != 0) { + ESP_LOGE(TAG, "MD5 computed: %s - Aborting due to MD5 mismatch", this->md5_computed_.c_str()); + return ota::OTA_RESPONSE_ERROR_MD5_MISMATCH; + } + + /* + ESP_LOGI(TAG, "Rebooting XMOS SoC..."); + if (!this->dfu_reboot_()) { + return OTA_COMMUNICATION_ERROR; + } + + delay(100); // NOLINT + + while (!this->dfu_get_version_()) { + delay(250); // NOLINT + // feed watchdog and give other tasks a chance to run + App.feed_wdt(); + yield(); + } + + ESP_LOGI(TAG, "Update complete"); + */ + + return ota::OTA_RESPONSE_OK; +} + + +void SatelliteFlasher::cleanup_(const std::shared_ptr &container) { + ESP_LOGV(TAG, "Aborting HTTP connection"); + container->end(); +}; + + + +} +} \ No newline at end of file diff --git a/esphome/components/satellite1/ota/flashing.h b/esphome/components/satellite1/ota/flashing.h new file mode 100644 index 00000000..171c94c6 --- /dev/null +++ b/esphome/components/satellite1/ota/flashing.h @@ -0,0 +1,76 @@ +#pragma once + +#include "esphome/components/http_request/http_request.h" +#include "esphome/components/ota/ota_backend.h" +#include "esphome/components/spi/spi.h" + +#include "esphome/components/satellite1/satellite1.h" + +namespace esphome { +namespace satellite1 { + +static const uint8_t MD5_SIZE = 32; + +enum SatelliteFlasherError : uint8_t { + OTA_MD5_INVALID = 0x10, + OTA_BAD_URL = 0x11, + OTA_CONNECTION_ERROR = 0x12, + OTA_INIT_FLASH_ERROR = 0x20, +}; + + + +class SatelliteFlasher : public ota::OTAComponent, + public Satellite1SPIService { +public: + void setup(); + void dump_config(); + + bool init_flasher(); + void dump_flash_info(); + bool deinit_flasher(); + + void set_http_request_component(http_request::HttpRequestComponent* http_request ){ + this->http_request_ = http_request; + } + void set_md5_url(const std::string &md5_url); + void set_md5(const std::string &md5) { this->md5_expected_ = md5; } + void set_url(const std::string &url); + + void flash(); + +protected: + GPIOPin* xmos_rst_n_{nullptr}; + + void read_JEDECID_(); + bool enable_writing_(); + bool disable_writing_(); + bool erase_sector_(int sector); + bool wait_while_flash_busy_(uint32_t timeout_ms); + bool read_page_( uint32_t byte_addr, uint8_t* buffer ); + bool write_page_( uint32_t byte_addr, uint8_t* buffer ); + uint8_t _manufacturerID; + uint8_t _memoryTypeID; + uint8_t _capacityID; + int32_t _capacity; + + bool http_get_md5_(); + bool validate_url_(const std::string &url); + void cleanup_(const std::shared_ptr &container); + + uint8_t write_to_flash_(); + + http_request::HttpRequestComponent* http_request_; + + std::string md5_computed_{}; + std::string md5_expected_{}; + std::string md5_url_{}; + std::string password_{}; + std::string username_{}; + std::string url_{}; + static const uint16_t HTTP_RECV_BUFFER = 256; +}; + + +} +} \ No newline at end of file diff --git a/esphome/components/satellite1/runtime_testing/__init__.py b/esphome/components/satellite1/runtime_testing/__init__.py new file mode 100644 index 00000000..fb1015b6 --- /dev/null +++ b/esphome/components/satellite1/runtime_testing/__init__.py @@ -0,0 +1,42 @@ +import esphome.codegen as cg +import esphome.config_validation as cv + +from esphome.const import ( + CONF_ID, + ) + + +from esphome.components.satellite1 import ( + CONF_SATELLITE1, + satellite1_ns, + Satellite1, + Satellite1SPIService +) + +DEPENDENCIES = ["spi"] +CODEOWNERS = ["@gnumpi"] + + + + +SPIErrorRate = satellite1_ns.class_("SPIErrorRate", cg.Component, Satellite1SPIService ) + + +CONFIG_SCHEMA = ( + cv.Schema({ + cv.GenerateID(): cv.declare_id(SPIErrorRate), + cv.GenerateID(CONF_SATELLITE1): cv.use_id(Satellite1), + }) +) + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await cg.register_parented(var, config[CONF_SATELLITE1]) + return var + + + + + + diff --git a/esphome/components/satellite1/runtime_testing/spi_error_rate.cpp b/esphome/components/satellite1/runtime_testing/spi_error_rate.cpp new file mode 100644 index 00000000..7d917d1f --- /dev/null +++ b/esphome/components/satellite1/runtime_testing/spi_error_rate.cpp @@ -0,0 +1,102 @@ +#include "testing.h" + +#include "esphome/core/log.h" + +namespace esphome { +namespace satellite1 { + +static const char *TAG = "Satellite1-Testing"; + +void SPIErrorRate::setup(){ + start_test(); +} + +void SPIErrorRate::loop(){ + if( this->start_time_ ){ + this->send_test_frame(); + this->read_test_frame(); + if( (millis() - this->start_time_) > 1000 ){ + this->report(); + this->start_time_ = millis(); + this->frames_received_ = 0; + this->incorrect_bytes_ = 0; + this->ignored_cmds_ = 0; + } + + + } +} + +void SPIErrorRate::start_test(){ + this->start_time_ = millis(); + this->waiting_ = false; + +} + +void SPIErrorRate::stop_test(){ + this->start_time_ = 0; +} + + +bool SPIErrorRate::send_test_frame(){ + if( !this->waiting_){ + for( int i = 0; i < this->bytes_per_frame_; i++ ){ + this->last_sent_[i] = rand() % 255; + } + if( this->parent_->transfer(ECHO_RES_ID, this->echo_cmd_ , this->last_sent_, this->bytes_per_frame_) ) + { + uint8_t *send_recv_buf = this->last_sent_; + //ESP_LOGD(TAG, "SEND: %x %x %x %x %x", send_recv_buf[0], send_recv_buf[1], send_recv_buf[2], send_recv_buf[3], send_recv_buf[4] ); + this->waiting_ = true; + return true; + } else { + this->ignored_cmds_++; + } + } + return false; +} + +bool SPIErrorRate::read_test_frame(){ + if( this->waiting_){ + uint8_t buffer[256]; + memset( buffer, 0, 256); + if( this->parent_->transfer(ECHO_RES_ID, this->echo_cmd_ | 0x80, buffer, this->bytes_per_frame_) ){ + for( int i = 0; i < this->bytes_per_frame_; i++ ){ + if( buffer[i] != this->last_sent_[i] ){ + this->incorrect_bytes_++; + } + } + this->frames_received_++; + this->waiting_ = false; + return true; + } else { + this->ignored_cmds_++; + } + } + return false; +} + + +bool SPIErrorRate::handle_response(uint8_t status, uint8_t res_id, uint8_t cmd, uint8_t* payload, uint8_t payload_len){ + return false; +}; + + + + +void SPIErrorRate::report(){ + uint32_t elapsed_time = (millis() - this->start_time_); + + ESP_LOGD(TAG, "Frames: %d, bytes/sec: %4.2f, error_rate: %4.2f incorrect: %d (failed: %d)", + this->frames_received_, + this->frames_received_ * this->bytes_per_frame_ * 1000. / elapsed_time, + this->frames_received_ ? 1. * this->incorrect_bytes_ / (this->bytes_per_frame_ * this->frames_received_) : 0., + this->incorrect_bytes_, + this->ignored_cmds_ ); +} + + + + +} +} \ No newline at end of file diff --git a/esphome/components/satellite1/runtime_testing/spi_error_rate.h b/esphome/components/satellite1/runtime_testing/spi_error_rate.h new file mode 100644 index 00000000..10063620 --- /dev/null +++ b/esphome/components/satellite1/runtime_testing/spi_error_rate.h @@ -0,0 +1,52 @@ +#pragma once + +#include "esphome/components/satellite1/satellite1.h" + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace satellite1 { + +const uint8_t ECHO_RES_ID = 230; + +const uint8_t ECHO_SERVICER_CMD_ECHO_1 = 0; +const uint8_t ECHO_SERVICER_CMD_ECHO_64 = 1; +const uint8_t ECHO_SERVICER_CMD_ECHO_128 = 2; + + +class SPIErrorRate : public Component, public SatelliteSPIService { +public: + void setup() override; + void loop() override; + + void start_test(); + void stop_test(); + + bool send_test_frame(); + bool read_test_frame(); + bool handle_response(uint8_t status, uint8_t res_id, uint8_t cmd, uint8_t* payload, uint8_t payload_len) override; + void report(); + +protected: + uint8_t resource_ids_[1] = {ECHO_RES_ID}; + + uint8_t bytes_per_frame_{128}; + uint8_t echo_cmd_{ECHO_SERVICER_CMD_ECHO_128}; + + uint8_t last_sent_[256]; + + uint32_t start_time_{0}; + uint32_t frames_received_{0}; + uint32_t ignored_cmds_{0}; + uint32_t incorrect_bytes_{0}; + bool waiting_{false}; +}; + + + + + + +} +} \ No newline at end of file diff --git a/esphome/components/satellite1/sat_gpio.cpp b/esphome/components/satellite1/sat_gpio.cpp new file mode 100644 index 00000000..f5840641 --- /dev/null +++ b/esphome/components/satellite1/sat_gpio.cpp @@ -0,0 +1,40 @@ +#include "sat_gpio.h" + +namespace esphome { +namespace satellite1 { + +static const char *TAG = "Satellite1-GPIOs"; + +void Satellite1GPIOPin::digital_write(bool value){ + if ( this->port_ != XMOSPort::OUTPUT_A ){ + ESP_LOGE(TAG, "Trying writing to read only port."); + return; + } + uint8_t payload[2] = { this->pin_, value }; + this->parent_->transfer( DC_RESOURCE::GPIO_PORT_OUT_A, GPIO_SERVICER_CMD_SET_PIN, payload, 2); +} + +bool Satellite1GPIOPin::digital_read(){ + DC_STATUS_REGISTER::register_id port_register; + switch (this->port_){ + case XMOSPort::INPUT_A: + port_register = DC_STATUS_REGISTER::GPIO_PORT_IN_A; + break; + case XMOSPort::INPUT_B: + port_register = DC_STATUS_REGISTER::GPIO_PORT_IN_B; + break; + case XMOSPort::OUTPUT_A: + port_register = DC_STATUS_REGISTER::GPIO_PORT_OUT_A; + break; + default: + ESP_LOGE(TAG, "Invalid port set."); + return 0; + break; + } + this->parent_->request_status_register_update(); + uint8_t port_value = this->parent_->get_dc_status( port_register ); + return !!( port_value & (1 << this->pin_) ) != this->inverted_; +} + +} +} \ No newline at end of file diff --git a/esphome/components/satellite1/sat_gpio.h b/esphome/components/satellite1/sat_gpio.h new file mode 100644 index 00000000..6b36d168 --- /dev/null +++ b/esphome/components/satellite1/sat_gpio.h @@ -0,0 +1,42 @@ +#pragma once + +#include "esphome/core/gpio.h" + +#include "satellite1.h" + +namespace esphome { +namespace satellite1 { + +static const uint8_t GPIO_SERVICER_CMD_READ_PORT = 0x00; +static const uint8_t GPIO_SERVICER_CMD_WRITE_PORT = 0x01; +static const uint8_t GPIO_SERVICER_CMD_SET_PIN = 0x02; + +enum class XMOSPort : uint8_t { + INPUT_A = 0, //buttons + INPUT_B, // rotary encoder + OUTPUT_A // explorer board LEDs +}; + +class Satellite1GPIOPin : public GPIOPin, public Satellite1SPIService { +public: + void setup() override {}; + void pin_mode(gpio::Flags flags) override {} + bool digital_read() override; + void digital_write(bool value) override; + std::string dump_summary() const override { return ""; }; + + void set_pin(XMOSPort port, uint8_t pin) { this->port_ = port; this->pin_ = pin; } + void set_inverted(bool inverted) { this->inverted_ = inverted; } + void set_flags(gpio::Flags flags) { this->flags_ = flags; } + +protected: + XMOSPort port_; + uint8_t pin_; + bool inverted_; + gpio::Flags flags_; +}; + + + +} +} \ No newline at end of file diff --git a/esphome/components/satellite1/satellite1.cpp b/esphome/components/satellite1/satellite1.cpp new file mode 100644 index 00000000..1ffd6961 --- /dev/null +++ b/esphome/components/satellite1/satellite1.cpp @@ -0,0 +1,113 @@ +#include "satellite1.h" +#include "esp_rom_gpio.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace satellite1 { + +static const char *TAG = "Satellite1"; + + +void Satellite1::setup(){ + esp_rom_gpio_pad_select_gpio( 40 ); + this->spi_setup(); + this->enable(); + this->transfer_byte(0); + this->disable(); + if( this->xmos_rst_pin_ ){ + this->xmos_rst_pin_->setup(); + } + if( this->flash_sw_pin_ ){ + this->flash_sw_pin_->setup(); + } +} + + +void Satellite1::dump_config(){ + esph_log_config(TAG, "Satellite1 config:"); + if( this->xmos_rst_pin_ ){ + this->xmos_rst_pin_->dump_summary(); + } else { + esph_log_config(TAG, " xmos_rst_pin not set up properly."); + } + if( this->flash_sw_pin_ ){ + this->flash_sw_pin_->dump_summary(); + }else { + esph_log_config(TAG, " flash_sw_pin not set up properly."); + } +} + + +bool Satellite1::request_status_register_update(){ + bool ret = this->transfer( 0, 0, NULL, 0 ); + uint8_t *arr = this->dc_status_register_; + return ret; +} + + +bool Satellite1::transfer( uint8_t resource_id, uint8_t command, uint8_t* payload, uint8_t payload_len){ + if( this->spi_flash_direct_access_enabled_ ){ + return false; + } + + uint8_t send_recv_buf[256+3] = {0}; + int status_report_dummies = std::max( 0, DC_STATUS_REGISTER::REGISTER_LEN - payload_len - 1); + + int attempts = 3; + do { + send_recv_buf[0] = resource_id; + send_recv_buf[1] = command; + send_recv_buf[2] = payload_len + !!(command & 0x80) ; + memcpy( &send_recv_buf[3], payload, payload_len ); + this->enable(); + this->transfer_array(&send_recv_buf[0], payload_len + 3 + status_report_dummies); + this->disable(); + vTaskDelay(1); + } while( send_recv_buf[0] == CONTROL_COMMAND_IGNORED_IN_DEVICE && attempts-- > 0 ); + + + if( send_recv_buf[0] == CONTROL_COMMAND_IGNORED_IN_DEVICE ) { + return false; + } + + // XMOS not responding at all + if( (send_recv_buf[0] + send_recv_buf[1] + send_recv_buf[2]) == 0 ) { + return false; + } + + // Got status register report + if( send_recv_buf[0] == DC_RESOURCE::CNTRL_ID && send_recv_buf[1] != DC_RET_STATUS::PAYLOAD_AVAILABLE ){ + memcpy( this->dc_status_register_, &send_recv_buf[2], DC_STATUS_REGISTER::REGISTER_LEN ); + uint8_t *arr = this->dc_status_register_; + } + + if( command & 0x80 ){ + attempts = 3; + do { + memset( send_recv_buf, 0, payload_len + 3); + this->enable(); + this->transfer_array(&send_recv_buf[0], payload_len + 3); + this->disable(); + vTaskDelay(1); + } while( send_recv_buf[0] == CONTROL_COMMAND_IGNORED_IN_DEVICE && attempts-- > 0 ); + + if( send_recv_buf[0] == CONTROL_COMMAND_IGNORED_IN_DEVICE ) { + return false; + } + + memcpy( payload, &send_recv_buf[1], payload_len ); + } + + return true; +} + + +void Satellite1::set_spi_flash_direct_access_mode(bool enable){ + this->xmos_rst_pin_->digital_write(enable); + this->flash_sw_pin_->digital_write(enable); + this->spi_flash_direct_access_enabled_ = enable; +} + + +} +} \ No newline at end of file diff --git a/esphome/components/satellite1/satellite1.h b/esphome/components/satellite1/satellite1.h new file mode 100644 index 00000000..71e33004 --- /dev/null +++ b/esphome/components/satellite1/satellite1.h @@ -0,0 +1,95 @@ +#pragma once + +#include "esphome/components/spi/spi.h" +#include "esphome/core/component.h" +#include "esphome/core/gpio.h" + +namespace esphome { +namespace satellite1 { + +//static const uint8_t CONTROL_STATUS_REGISTER_LEN = 4; + +static const uint8_t CONTROL_RESOURCE_CNTRL_ID = 1; + +static const uint8_t RET_STATUS_PAYLOAD_AVAIL = 23; + +static const uint8_t CONTROL_COMMAND_IGNORED_IN_DEVICE = 7; + +static const uint8_t GPIO_SERVICER_RESID_PORT_IN_A = 211; +static const uint8_t GPIO_SERVICER_RESID_PORT_IN_B = 212; +static const uint8_t GPIO_SERVICER_RESID_PORT_OUT_A = 221; + + +namespace DC_RESOURCE { +enum dc_resource_enum { + CNTRL_ID = 1, + GPIO_PORT_IN_A = GPIO_SERVICER_RESID_PORT_IN_A, + GPIO_PORT_IN_B = GPIO_SERVICER_RESID_PORT_IN_B, + GPIO_PORT_OUT_A = GPIO_SERVICER_RESID_PORT_OUT_A +}; +} + +namespace DC_RET_STATUS { +enum dc_ret_status_enum { + CMD_SUCCESS = 0, + DEVICE_BUSY = 7, + PAYLOAD_AVAILABLE = 23 +}; +} + +namespace DC_STATUS_REGISTER { +enum register_id { + DEVICE_STATUS = 0, + GPIO_PORT_IN_A = 1, + GPIO_PORT_IN_B = 2, + GPIO_PORT_OUT_A = 3, + + REGISTER_LEN = 4 +}; +} + + +class Satellite1 : public Component, + public spi::SPIDevice { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::IO; } + + bool transfer( uint8_t resource_id, uint8_t command, uint8_t* payload, uint8_t payload_len); + + bool request_status_register_update(); + uint8_t get_dc_status( DC_STATUS_REGISTER::register_id reg){assert(reg < DC_STATUS_REGISTER::REGISTER_LEN); return this->dc_status_register_[reg]; } + + void set_xmos_rst_pin(GPIOPin* xmos_rst_pin){this->xmos_rst_pin_ = xmos_rst_pin;} + void set_flash_sw_pin(GPIOPin* flash_sw_pin){this->flash_sw_pin_ = flash_sw_pin;} + + void set_spi_flash_direct_access_mode(bool enable); +protected: + uint8_t dc_status_register_[DC_STATUS_REGISTER::REGISTER_LEN]; + bool spi_flash_direct_access_enabled_{false}; + + GPIOPin* xmos_rst_pin_{nullptr}; + GPIOPin* flash_sw_pin_{nullptr}; +}; + + + +class Satellite1SPIService : public Parented { +public: + virtual bool handle_response(uint8_t status, uint8_t res_id, uint8_t cmd, uint8_t* payload, uint8_t payload_len){return false;} + +protected: + uint8_t transfer_byte(uint8_t byte ) {return this->parent_->transfer_byte(byte);} + void enable() {this->parent_->enable();} + void disable(){this->parent_->disable();} + uint8_t servicer_id_; + +}; + + + + +} +} \ No newline at end of file diff --git a/esphome/components/tas2780/__init__.py b/esphome/components/tas2780/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/esphome/components/tas2780/audio_dac.py b/esphome/components/tas2780/audio_dac.py new file mode 100644 index 00000000..5b9c45d4 --- /dev/null +++ b/esphome/components/tas2780/audio_dac.py @@ -0,0 +1,29 @@ +import esphome.codegen as cg +from esphome.components import i2c +import esphome.config_validation as cv +from esphome import automation +from esphome.components.audio_dac import AudioDac, audio_dac_ns +from esphome.const import CONF_ID, CONF_MODE + +CODEOWNERS = ["@gnumpi"] +DEPENDENCIES = ["i2c"] + +tas2780_ns = cg.esphome_ns.namespace("tas2780") +tas2780 = tas2780_ns.class_("TAS2780", AudioDac, cg.Component, i2c.I2CDevice) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(tas2780), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x18)) +) + + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) \ No newline at end of file diff --git a/esphome/components/tas2780/tas2780.cpp b/esphome/components/tas2780/tas2780.cpp new file mode 100644 index 00000000..6a7c14d3 --- /dev/null +++ b/esphome/components/tas2780/tas2780.cpp @@ -0,0 +1,125 @@ +#include "tas2780.h" + +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace tas2780 { + +static const char *const TAG = "tas2780"; + +static const uint8_t TAS2780_REG00_PAGE_SELECT = 0x00; // Page Select + +void TAS2780::setup(){ + // select page 0 + this->reg(TAS2780_REG00_PAGE_SELECT) = 0x00; + + // software reset + this->reg(0x01) = 0x01; + + uint8_t chd1 = this->reg(0x05).get(); + uint8_t chd2 = this->reg(0x68).get(); + uint8_t chd3 = this->reg(0x02).get(); + + if( chd1 == 0x41 ){ + ESP_LOGD(TAG, "TAS2780 chip found."); + ESP_LOGD(TAG, "Reg 0x68: %d.", chd2 ); + ESP_LOGD(TAG, "Reg 0x02: %d.", chd3 ); + } + else + { + ESP_LOGD(TAG, "TAS2780 chip not found."); + this->mark_failed(); + return; + } + + this->reg(TAS2780_REG00_PAGE_SELECT) = 0x00; + this->reg(0x0e) = 0x44; + this->reg(0x0f) = 0x40; + + + this->reg(TAS2780_REG00_PAGE_SELECT) = 0x01; + this->reg(0x17) = 0xc0; + this->reg(0x19) = 0x00; + this->reg(0x21) = 0x00; + this->reg(0x35) = 0x74; + + this->reg(TAS2780_REG00_PAGE_SELECT) = 0xFD; + this->reg(0x0d) = 0x0d; + this->reg(0x3e) = 0x4a; + this->reg(0x0d) = 0x00; + + + this->reg(TAS2780_REG00_PAGE_SELECT) = 0x00; + //Power Mode 2 (no external VBAT) + this->reg(0x03) = 0xe8; + this->reg(0x04) = 0xa1; + this->reg(0x71) = 0x12; + + // activate + //uint8_t reg2 = this->reg(0x02).get(); + this->reg(0x02) = 0x80; + + } + +void TAS2780::loop(){ +#if 1 + uint8_t reg2 = this->reg(0x02).get(); + ESP_LOGD(TAG, "Reg 0x02: %d.", reg2 ); + + reg2 = this->reg(0x49).get(); + ESP_LOGD(TAG, "Reg 0x49: %d.", reg2 ); + reg2 = this->reg(0x4A).get(); + ESP_LOGD(TAG, "Reg 0x4A: %d.", reg2 ); + reg2 = this->reg(0x4B).get(); + ESP_LOGD(TAG, "Reg 0x4B: %d.", reg2 ); + reg2 = this->reg(0x4F).get(); + ESP_LOGD(TAG, "Reg 0x4F: %d.", reg2 ); + reg2 = this->reg(0x50).get(); + ESP_LOGD(TAG, "Reg 0x50: %d.\n", reg2 ); + delay(5); +#endif +} + + + +void TAS2780::dump_config(){ + +} + +bool TAS2780::set_mute_off(){ + this->is_muted_ = false; + return this->write_mute_(); +} + +bool TAS2780::set_mute_on(){ + this->is_muted_ = true; + return this->write_mute_(); +} + +bool TAS2780::set_volume(float volume) { + this->volume_ = clamp(volume, 0.0, 1.0); + return this->write_volume_(); +} + +bool TAS2780::is_muted() { + return this->is_muted_; +} + +float TAS2780::volume() { + return this->volume_; +} + +bool TAS2780::write_mute_() { + return true; +} + +bool TAS2780::write_volume_() { + return true; +} + + + +} +} \ No newline at end of file diff --git a/esphome/components/tas2780/tas2780.h b/esphome/components/tas2780/tas2780.h new file mode 100644 index 00000000..cb7a43d4 --- /dev/null +++ b/esphome/components/tas2780/tas2780.h @@ -0,0 +1,34 @@ +#pragma once + +#include "esphome/components/audio_dac/audio_dac.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace tas2780 { + +class TAS2780 : public audio_dac::AudioDac, public Component, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void loop() override; + + bool set_mute_off() override; + bool set_mute_on() override; + bool set_volume(float volume) override; + + bool is_muted() override; + float volume() override; + + protected: + bool write_mute_(); + bool write_volume_(); + + float volume_{0}; +}; + +} +} \ No newline at end of file diff --git a/esphome/components/udp_stream/__init__.py b/esphome/components/udp_stream/__init__.py new file mode 100644 index 00000000..84870a37 --- /dev/null +++ b/esphome/components/udp_stream/__init__.py @@ -0,0 +1,127 @@ +import esphome.config_validation as cv +import esphome.codegen as cg + +from esphome.const import ( + CONF_ID, + CONF_MICROPHONE, + CONF_IP_ADDRESS +) +from esphome import automation +from esphome.automation import register_action, register_condition +from esphome.components import microphone +from esphome.components.network import IPAddress + +import socket + +AUTO_LOAD = ["socket"] +DEPENDENCIES = ["microphone"] + +CODEOWNERS = ["@gnumpi"] + +CONF_ON_END = "on_end" +CONF_ON_ERROR = "on_error" +CONF_ON_START = "on_start" + +udp_stream_ns = cg.esphome_ns.namespace("udp_stream") +UDPStreamer = udp_stream_ns.class_("UDPStreamer", cg.Component) + +StartAction = udp_stream_ns.class_( + "StartAction", automation.Action, cg.Parented.template(UDPStreamer) +) +StartContinuousAction = udp_stream_ns.class_( + "StartContinuousAction", automation.Action, cg.Parented.template(UDPStreamer) +) +StopAction = udp_stream_ns.class_( + "StopAction", automation.Action, cg.Parented.template(UDPStreamer) +) +IsRunningCondition = udp_stream_ns.class_( + "IsRunningCondition", automation.Condition, cg.Parented.template(UDPStreamer) +) + +def get_local_ip() -> str | None : + local_hostname = socket.gethostname() + ip_addresses = socket.gethostbyname_ex(local_hostname)[2] + filtered_ips = [ip for ip in ip_addresses if not ip.startswith("127.")] + return filtered_ips[0] if len(filtered_ips) else None + +def safe_ip(ip): + if ip is None: + return IPAddress(0, 0, 0, 0) + return IPAddress(*ip.args) + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(UDPStreamer), + cv.GenerateID(CONF_MICROPHONE): cv.use_id(microphone.Microphone), + cv.Optional(CONF_IP_ADDRESS, default=get_local_ip()) : cv.ipv4, + cv.Optional(CONF_ON_START): automation.validate_automation(single=True), + cv.Optional(CONF_ON_END): automation.validate_automation(single=True), + cv.Optional(CONF_ON_ERROR): automation.validate_automation(single=True), + } + ).extend(cv.COMPONENT_SCHEMA) +) + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + mic = await cg.get_variable(config[CONF_MICROPHONE]) + cg.add(var.set_microphone(mic)) + cg.add(var.set_remote_ip(safe_ip(config[CONF_IP_ADDRESS]))) + + if CONF_ON_START in config: + await automation.build_automation( + var.get_start_trigger(), [], config[CONF_ON_START] + ) + + if CONF_ON_END in config: + await automation.build_automation( + var.get_end_trigger(), [], config[CONF_ON_END] + ) + + if CONF_ON_ERROR in config: + await automation.build_automation( + var.get_error_trigger(), + [(cg.std_string, "code"), (cg.std_string, "message")], + config[CONF_ON_ERROR], + ) + + + +UDP_STREAMER_ACTION_SCHEMA = cv.Schema({cv.GenerateID(): cv.use_id(UDPStreamer)}) + + +@register_action( + "udp_stream.start_continuous", + StartContinuousAction, + UDP_STREAMER_ACTION_SCHEMA, +) +@register_action( + "udp_stream.start", + StartAction, + UDP_STREAMER_ACTION_SCHEMA +) +async def voice_assistant_listen_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +@register_action("udp_stream.stop", StopAction, UDP_STREAMER_ACTION_SCHEMA) +async def voice_assistant_stop_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +@register_condition( + "udp_stream.is_running", IsRunningCondition, UDP_STREAMER_ACTION_SCHEMA +) +async def voice_assistant_is_running_to_code(config, condition_id, template_arg, args): + var = cg.new_Pvariable(condition_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + diff --git a/esphome/components/udp_stream/udp_stream.cpp b/esphome/components/udp_stream/udp_stream.cpp new file mode 100644 index 00000000..a6a5d5ce --- /dev/null +++ b/esphome/components/udp_stream/udp_stream.cpp @@ -0,0 +1,289 @@ +#include "udp_stream.h" + +#include "esphome/core/log.h" + +#include +#include + +namespace esphome { +namespace udp_stream { + +static const char *const TAG = "udp_streamer"; + +#ifdef SAMPLE_RATE_HZ +#undef SAMPLE_RATE_HZ +#endif + +static const size_t SAMPLE_RATE_HZ = 16000; +static const size_t INPUT_BUFFER_SIZE = 32 * SAMPLE_RATE_HZ / 1000; // 32ms * 16kHz / 1000ms +static const size_t BUFFER_SIZE = 512 * SAMPLE_RATE_HZ / 1000; +static const size_t SEND_BUFFER_SIZE = INPUT_BUFFER_SIZE * sizeof(int16_t); +static const size_t RECEIVE_SIZE = 1024; +static const size_t SPEAKER_BUFFER_SIZE = 16 * RECEIVE_SIZE; + +float UDPStreamer::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; } + +bool UDPStreamer::start_udp_socket_() { + struct sockaddr_in server_addr; + memset(&server_addr, 0, sizeof(server_addr)); + memset(&this->dest_addr_, 0, sizeof(this->dest_addr_)); + + server_addr.sin_family = AF_INET; + server_addr.sin_port = htons(6055); // Port number in network byte order + + esp_ip_addr_t esp_ip = this->remote_ip_; + server_addr.sin_addr.s_addr = esp_ip.u_addr.ip4.addr; + + memcpy(&this->dest_addr_, &server_addr, sizeof(server_addr)); + this->socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); + if (this->socket_ == nullptr) { + ESP_LOGE(TAG, "Could not create socket"); + this->mark_failed(); + return false; + } + int enable = 1; + int err = this->socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)); + if (err != 0) { + ESP_LOGW(TAG, "Socket unable to set reuseaddr: errno %d", err); + // we can still continue + } + err = this->socket_->setblocking(false); + if (err != 0) { + ESP_LOGE(TAG, "Socket unable to set nonblocking mode: errno %d", err); + this->mark_failed(); + return false; + } + + this->udp_socket_running_ = true; + return true; +} + +void UDPStreamer::setup() { + ESP_LOGCONFIG(TAG, "Setting up UDP Streamer..."); +} + +bool UDPStreamer::allocate_buffers_() { + if (this->send_buffer_ != nullptr) { + return true; // Already allocated + } + + ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + this->input_buffer_ = allocator.allocate(INPUT_BUFFER_SIZE); + if (this->input_buffer_ == nullptr) { + ESP_LOGW(TAG, "Could not allocate input buffer"); + return false; + } + + this->ring_buffer_ = RingBuffer::create(BUFFER_SIZE * sizeof(int16_t)); + if (this->ring_buffer_ == nullptr) { + ESP_LOGW(TAG, "Could not allocate ring buffer"); + return false; + } + + ExternalRAMAllocator send_allocator(ExternalRAMAllocator::ALLOW_FAILURE); + this->send_buffer_ = send_allocator.allocate(SEND_BUFFER_SIZE); + if (send_buffer_ == nullptr) { + ESP_LOGW(TAG, "Could not allocate send buffer"); + return false; + } + + return true; +} + +void UDPStreamer::clear_buffers_() { + if (this->send_buffer_ != nullptr) { + memset(this->send_buffer_, 0, SEND_BUFFER_SIZE); + } + + if (this->input_buffer_ != nullptr) { + memset(this->input_buffer_, 0, INPUT_BUFFER_SIZE * sizeof(int16_t)); + } + + if (this->ring_buffer_ != nullptr) { + this->ring_buffer_->reset(); + } + +} + +void UDPStreamer::deallocate_buffers_() { + ExternalRAMAllocator send_deallocator(ExternalRAMAllocator::ALLOW_FAILURE); + send_deallocator.deallocate(this->send_buffer_, SEND_BUFFER_SIZE); + this->send_buffer_ = nullptr; + + if (this->ring_buffer_ != nullptr) { + this->ring_buffer_.reset(); + this->ring_buffer_ = nullptr; + } + + + ExternalRAMAllocator input_deallocator(ExternalRAMAllocator::ALLOW_FAILURE); + input_deallocator.deallocate(this->input_buffer_, INPUT_BUFFER_SIZE); + this->input_buffer_ = nullptr; +} + +int UDPStreamer::read_microphone_() { + size_t bytes_read = 0; + if (this->mic_->is_running()) { // Read audio into input buffer + bytes_read = this->mic_->read(this->input_buffer_, INPUT_BUFFER_SIZE * sizeof(int16_t)); + if (bytes_read == 0) { + memset(this->input_buffer_, 0, INPUT_BUFFER_SIZE * sizeof(int16_t)); + return 0; + } + // Write audio into ring buffer + this->ring_buffer_->write((void *) this->input_buffer_, bytes_read); + } else { + ESP_LOGD(TAG, "microphone not running"); + } + return bytes_read; +} + +void UDPStreamer::loop() { + switch (this->state_) { + case State::IDLE: { + if (this->continuous_ && this->desired_state_ == State::IDLE) { + this->idle_trigger_->trigger(); + { + this->set_state_(State::START_MICROPHONE, State::STREAMING_MICROPHONE); + } + } else { + this->high_freq_.stop(); + } + break; + } + case State::START_MICROPHONE: { + ESP_LOGD(TAG, "Starting Microphone"); + if (!this->allocate_buffers_()) { + this->status_set_error("Failed to allocate buffers"); + return; + } + if (this->status_has_error()) { + this->status_clear_error(); + } + this->clear_buffers_(); + + this->mic_->start(); + this->high_freq_.start(); + this->set_state_(State::STARTING_MICROPHONE); + break; + } + case State::STARTING_MICROPHONE: { + if (this->mic_->is_running()) { + this->set_state_(this->desired_state_); + } + break; + } + case State::STREAMING_MICROPHONE: { + this->read_microphone_(); + size_t available = this->ring_buffer_->available(); + while (available >= SEND_BUFFER_SIZE) { + size_t read_bytes = this->ring_buffer_->read((void *) this->send_buffer_, SEND_BUFFER_SIZE, 0); + if (!this->udp_socket_running_) { + if (!this->start_udp_socket_()) { + this->set_state_(State::STOP_MICROPHONE, State::IDLE); + break; + } + } + this->socket_->sendto(this->send_buffer_, read_bytes, 0, (struct sockaddr *) &this->dest_addr_, + sizeof(this->dest_addr_)); + available = this->ring_buffer_->available(); + } + + break; + } + case State::STOP_MICROPHONE: { + if (this->mic_->is_running()) { + this->mic_->stop(); + this->set_state_(State::STOPPING_MICROPHONE); + } else { + this->set_state_(this->desired_state_); + } + break; + } + case State::STOPPING_MICROPHONE: { + if (this->mic_->is_stopped()) { + this->set_state_(this->desired_state_); + } + break; + } + default: + break; + } +} + + +static const LogString *voice_assistant_state_to_string(State state) { + switch (state) { + case State::IDLE: + return LOG_STR("IDLE"); + case State::START_MICROPHONE: + return LOG_STR("START_MICROPHONE"); + case State::STARTING_MICROPHONE: + return LOG_STR("STARTING_MICROPHONE"); + case State::STREAMING_MICROPHONE: + return LOG_STR("STREAMING_MICROPHONE"); + case State::STOP_MICROPHONE: + return LOG_STR("STOP_MICROPHONE"); + case State::STOPPING_MICROPHONE: + return LOG_STR("STOPPING_MICROPHONE"); + default: + return LOG_STR("UNKNOWN"); + } +}; + +void UDPStreamer::set_state_(State state) { + State old_state = this->state_; + this->state_ = state; + ESP_LOGD(TAG, "State changed from %s to %s", LOG_STR_ARG(voice_assistant_state_to_string(old_state)), + LOG_STR_ARG(voice_assistant_state_to_string(state))); +} + +void UDPStreamer::set_state_(State state, State desired_state) { + this->set_state_(state); + this->desired_state_ = desired_state; + ESP_LOGD(TAG, "Desired state set to %s", LOG_STR_ARG(voice_assistant_state_to_string(desired_state))); +} + +void UDPStreamer::failed_to_start() { + ESP_LOGE(TAG, "Failed to start server. See Home Assistant logs for more details."); + this->error_trigger_->trigger("failed-to-start", "Failed to start server. See Home Assistant logs for more details."); + this->set_state_(State::STOP_MICROPHONE, State::IDLE); +} + + +void UDPStreamer::request_start(bool continuous) { + if (this->state_ == State::IDLE) { + this->continuous_ = continuous; + this->set_state_(State::START_MICROPHONE, State::STREAMING_MICROPHONE); + } +} + +void UDPStreamer::request_stop() { + this->continuous_ = false; + + switch (this->state_) { + case State::IDLE: + break; + case State::START_MICROPHONE: + case State::STARTING_MICROPHONE: + this->set_state_(State::STOP_MICROPHONE, State::IDLE); + break; + case State::STREAMING_MICROPHONE: + this->signal_stop_(); + this->set_state_(State::STOP_MICROPHONE, State::IDLE); + break; + case State::STOP_MICROPHONE: + case State::STOPPING_MICROPHONE: + this->desired_state_ = State::IDLE; + break; + } +} + +void UDPStreamer::signal_stop_() { + memset(&this->dest_addr_, 0, sizeof(this->dest_addr_)); + this->udp_socket_running_ = false; +} + + +} // namespace voice_assistant +} // namespace esphome + diff --git a/esphome/components/udp_stream/udp_stream.h b/esphome/components/udp_stream/udp_stream.h new file mode 100644 index 00000000..932d74ab --- /dev/null +++ b/esphome/components/udp_stream/udp_stream.h @@ -0,0 +1,120 @@ +#pragma once + +#include "esphome/core/defines.h" + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "esphome/core/ring_buffer.h" + +#include "esphome/components/microphone/microphone.h" +#include "esphome/components/network/ip_address.h" +#include "esphome/components/socket/socket.h" + +#include +#include + +namespace esphome { +namespace udp_stream { + +enum class State { + IDLE, + START_MICROPHONE, + STARTING_MICROPHONE, + STREAMING_MICROPHONE, + STOP_MICROPHONE, + STOPPING_MICROPHONE, +}; + + +class UDPStreamer : public Component { + public: + void setup() override; + void loop() override; + float get_setup_priority() const override; + void failed_to_start(); + + void set_microphone(microphone::Microphone *mic) { this->mic_ = mic; } + void set_remote_udp_port(uint16_t port){ this->remote_port_ = port; } + void set_remote_ip(struct esphome::network::IPAddress ip_addr){ this->remote_ip_ = ip_addr; } + + void request_start(bool continuous); + void request_stop(); + + bool is_running() const { return this->state_ != State::IDLE; } + void set_continuous(bool continuous) { this->continuous_ = continuous; } + bool is_continuous() const { return this->continuous_; } + + Trigger<> *get_end_trigger() const { return this->end_trigger_; } + Trigger<> *get_start_trigger() const { return this->start_trigger_; } + Trigger *get_error_trigger() const { return this->error_trigger_; } + Trigger<> *get_idle_trigger() const { return this->idle_trigger_; } + + protected: + bool allocate_buffers_(); + void clear_buffers_(); + void deallocate_buffers_(); + + int read_microphone_(); + void set_state_(State state); + void set_state_(State state, State desired_state); + void signal_stop_(); + + std::unique_ptr socket_ = nullptr; + struct sockaddr_storage dest_addr_; + uint16_t remote_port_; + struct esphome::network::IPAddress remote_ip_; + + Trigger<> *listening_trigger_ = new Trigger<>(); + Trigger<> *end_trigger_ = new Trigger<>(); + Trigger<> *start_trigger_ = new Trigger<>(); + Trigger *error_trigger_ = new Trigger(); + Trigger<> *idle_trigger_ = new Trigger<>(); + + microphone::Microphone *mic_{nullptr}; + + bool local_output_{false}; + + HighFrequencyLoopRequester high_freq_; + + std::unique_ptr ring_buffer_; + + uint8_t *send_buffer_; + int16_t *input_buffer_; + + bool continuous_{false}; + + State state_{State::IDLE}; + State desired_state_{State::IDLE}; + + bool udp_socket_running_{false}; + bool start_udp_socket_(); +}; + +template class StartAction : public Action, public Parented { + TEMPLATABLE_VALUE(std::string, wake_word); + + public: + void play(Ts... x) override { + this->parent_->request_start(false); + } +}; + +template class StartContinuousAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->request_start(true); } +}; + +template class StopAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->request_stop(); } +}; + +template class IsRunningCondition : public Condition, public Parented { + public: + bool check(Ts... x) override { return this->parent_->is_running() || this->parent_->is_continuous(); } +}; + +} // namespace udp_streamer +} // namespace esphome + diff --git a/esphome/components/version/__init__.py b/esphome/components/version/__init__.py new file mode 100644 index 00000000..f31fb4c1 --- /dev/null +++ b/esphome/components/version/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@esphome/core", "@gnumpi"] diff --git a/esphome/components/version/helpers.py b/esphome/components/version/helpers.py new file mode 100644 index 00000000..9dc147e8 --- /dev/null +++ b/esphome/components/version/helpers.py @@ -0,0 +1,27 @@ +import subprocess + +def has_changed_files(): + try: + # Run the 'git status --porcelain' command + result = subprocess.run(['git', 'status', '--porcelain'], capture_output=True, text=True, check=True) + + # Check if the output is not empty + if result.stdout.strip(): + return True + else: + return False + except subprocess.CalledProcessError as e: + print(f"An error occurred while checking the Git status: {e}") + return False + +def get_current_commit_hash(): + try: + # Run the 'git rev-parse HEAD' command + result = subprocess.run(['git', 'rev-parse', '--short=10' ,'HEAD'], capture_output=True, text=True, check=True) + + # Return the commit hash, stripping any trailing newline + return result.stdout.strip() + except subprocess.CalledProcessError as e: + print(f"An error occurred while getting the current commit hash: {e}") + return None + diff --git a/esphome/components/version/text_sensor.py b/esphome/components/version/text_sensor.py new file mode 100644 index 00000000..36c7bf80 --- /dev/null +++ b/esphome/components/version/text_sensor.py @@ -0,0 +1,45 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import text_sensor +from esphome.const import ( + ENTITY_CATEGORY_DIAGNOSTIC, + ICON_NEW_BOX, + CONF_HIDE_TIMESTAMP, +) + +from .helpers import has_changed_files, get_current_commit_hash + +version_ns = cg.esphome_ns.namespace("version") +VersionTextSensor = version_ns.class_( + "VersionTextSensor", text_sensor.TextSensor, cg.Component +) + +def validate_git(config): + if has_changed_files(): + raise cv.Invalid("Please commit all local changes before building the firmware.") + return config + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + text_sensor.text_sensor_schema( + icon=ICON_NEW_BOX, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ) + .extend( + { + cv.GenerateID(): cv.declare_id(VersionTextSensor), + cv.Optional(CONF_HIDE_TIMESTAMP, default=False): cv.boolean, + } + ) + .extend(cv.COMPONENT_SCHEMA) + ), + #validate_git +) + + +async def to_code(config): + var = await text_sensor.new_text_sensor(config) + await cg.register_component(var, config) + cg.add(var.set_hide_timestamp(config[CONF_HIDE_TIMESTAMP])) + cg.add(var.set_git_commit(get_current_commit_hash())) diff --git a/esphome/components/version/version_text_sensor.cpp b/esphome/components/version/version_text_sensor.cpp new file mode 100644 index 00000000..db59edeb --- /dev/null +++ b/esphome/components/version/version_text_sensor.cpp @@ -0,0 +1,24 @@ +#include "version_text_sensor.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" +#include "esphome/core/version.h" + +namespace esphome { +namespace version { + +static const char *const TAG = "version.text_sensor"; + +void VersionTextSensor::setup() { + if (this->hide_timestamp_) { + this->publish_state(ESPHOME_VERSION "+" + this->git_commit_); + } else { + this->publish_state(ESPHOME_VERSION "+" + this->git_commit_ + " " + App.get_compilation_time()); + } +} +float VersionTextSensor::get_setup_priority() const { return setup_priority::DATA; } +void VersionTextSensor::set_hide_timestamp(bool hide_timestamp) { this->hide_timestamp_ = hide_timestamp; } +std::string VersionTextSensor::unique_id() { return get_mac_address() + "-version"; } +void VersionTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Version Text Sensor", this); } + +} // namespace version +} // namespace esphome diff --git a/esphome/components/version/version_text_sensor.h b/esphome/components/version/version_text_sensor.h new file mode 100644 index 00000000..33675a4e --- /dev/null +++ b/esphome/components/version/version_text_sensor.h @@ -0,0 +1,24 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/text_sensor/text_sensor.h" + +namespace esphome { +namespace version { + +class VersionTextSensor : public text_sensor::TextSensor, public Component { + public: + void set_hide_timestamp(bool hide_timestamp); + void set_git_commit(std::string commit_hash){ this->git_commit_ = commit_hash; } + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + std::string unique_id() override; + + protected: + bool hide_timestamp_{false}; + std::string git_commit_{}; +}; + +} // namespace version +} // namespace esphome diff --git a/requirements.txt b/requirements.txt index 7e0cf75d..0bc0be7e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -esphome==2024.6 +esphome==2024.11.2 setuptools \ No newline at end of file diff --git a/tests/components/fusb302b/test_power_delivery.yaml b/tests/components/fusb302b/test_power_delivery.yaml new file mode 100644 index 00000000..cf97524e --- /dev/null +++ b/tests/components/fusb302b/test_power_delivery.yaml @@ -0,0 +1,85 @@ +substitutions: + #Change to any preferred name + friendly_name: "Satellite1 Power Delivery Testing" + + #Recommend leaving the following unchanged + node_name: sat1-usb-pd-testing + company_name: FutureProofHomes + project_name: Satellite1 + component_name: Core + +esphome: + name: ${node_name} + name_add_mac_suffix: true + friendly_name: ${friendly_name} + min_version: 2024.9.0 + + project: + name: ${company_name}.${project_name} + version: dev + + +packages: + device_base: !include common/core_board.yaml + wifi: !include common/wifi_improv.yaml + +logger: + deassert_rts_dtr: true + hardware_uart : USB_SERIAL_JTAG + level: VERBOSE + +api: + +external_components: + - source: + type: local + path: ../esphome/components + components: [ fusb302b ] + + + +fusb302b: + id: pd_fusb302b + irq_pin: GPIO1 + request_voltage: 20 + on_power_ready: + then: + - logger.log: + format: "PD contract got accepted: %s" + args: [ 'id(pd_fusb302b).contract.c_str()' ] + - text_sensor.template.publish: + id: pd_state_text + state: !lambda 'return id(pd_fusb302b).contract;' + on_disconnect: + - text_sensor.template.publish: + id: pd_state_text + state: "Disconnected" + + +text_sensor: + - platform: template + id: pd_state_text + name: "USB-C (PD State)" + entity_category: "diagnostic" + icon: "mdi:code-braces" + lambda: |- + if( id(pd_fusb302b).state == power_delivery::PD_STATE_DISCONNECTED){ + return std::string("Disconnected"); + } else { + return id(pd_fusb302b).contract; + } + + +button: + - platform: template + id: request_9V_button + name: "PD Request 9V" + entity_category: diagnostic + on_press: + then: + - power_delivery.request_voltage: + request_voltage: 9 + +script: + - id: control_leds + then: \ No newline at end of file diff --git a/tests/mic_streaming/README.md b/tests/mic_streaming/README.md new file mode 100644 index 00000000..f9007ca1 --- /dev/null +++ b/tests/mic_streaming/README.md @@ -0,0 +1,76 @@ +# Test Microphone Pipeline + +Audio files from the wake-word-benchmark are played on this machine while the microphone signal is received via UDP from the satellite and recorded. + +### Setup + +1. install benchmark files + + ```sh + tests/mic_streaming/setup_testdata.sh + ``` +2. if not already done, install build environment + ```sh + source scripts/setup_build_env.sh + ``` + +3. if not already done, activate virtual env + ```sh + source .venv/bin/activate + ``` + +4. install additional requirements: + ```sh + pip install -r tests/mic_streaming/requirements.txt + ``` + +5. compile & upload firmware (this will set the udp server ip-address to this machine) + ```sh + esphome compile config/test_stream_mics.yaml + esphome upload config/test_stream_mics.yaml + ``` + +### Run Test + +1. if not already done, activate virtual env + ```sh + source .venv/bin/activate + ``` + +2. enable `Stream Mic via UDP` switch in HA + +3. run test: + ``` + python tests/mic_streaming/run_test.py + ``` + +4. disable `Stream Mic via UDP` switch in HA + + +### Run Live Streaming +The received mic data is played directly played on this machine and recorded in 10s chunks. +Make sure to use headphones in order to prevent audio feedback. + +1. if not already done, activate virtual env + ```sh + source .venv/bin/activate + ``` + +2. run test: + ``` + python tests/mic_streaming/run_live_streaming.py + ``` + +3. enable `Stream Mic via UDP` switch in HA + + +4. disable `Stream Mic via UDP` switch in HA + + + + +### Recordings +Recordings can be found here: +``` +testdata/mic_streaming/ +``` \ No newline at end of file diff --git a/tests/mic_streaming/requirements.txt b/tests/mic_streaming/requirements.txt new file mode 100644 index 00000000..7d381f8e --- /dev/null +++ b/tests/mic_streaming/requirements.txt @@ -0,0 +1 @@ +pyaudio \ No newline at end of file diff --git a/tests/mic_streaming/run_live_streaming.py b/tests/mic_streaming/run_live_streaming.py new file mode 100644 index 00000000..5934e1d2 --- /dev/null +++ b/tests/mic_streaming/run_live_streaming.py @@ -0,0 +1,60 @@ +import pyaudio +import socket +import wave +import os +from datetime import datetime + +""" +Listen on udp port 6055 for audio data and stream directly to output speaker. +""" +FORMAT = pyaudio.paInt16 # Format of sampling +CHANNELS = 1 # Number of audio channels (1 for mono, 2 for stereo) +RATE = 16000 # Sampling rate (samples per second) +CHUNK = 1024 # Number of audio frames per buffer +RECORD_SECONDS = 10 # Duration of recording +PORT = 6055 + +""" +Store records under test_runs/CURRENT_DATETIME +""" +ROOT_DIR = os.path.join( os.path.dirname(__file__), "../../") +TEST_RUN_ROOT = os.path.join(ROOT_DIR, "testdata", "mic_streaming" ) +TEST_RUN_DIR = os.path.join(TEST_RUN_ROOT, datetime.now().isoformat() ) +os.makedirs( TEST_RUN_DIR ) + + +# Create a UDP socket +sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) +sock.bind(("0.0.0.0", PORT)) + +# Create an audio object +p = pyaudio.PyAudio() + +# Open stream +stream = p.open(format=pyaudio.paInt16, channels=CHANNELS, rate=RATE, output=True) + +file_idx = 0 +chunks = [] +while True: + try: + data, addr = sock.recvfrom(CHUNK) + chunks.append(data) + if len(chunks) == int(RATE / CHUNK * RECORD_SECONDS) : + with wave.open( os.path.join( TEST_RUN_DIR, f"rec_{file_idx:03d}.wav"), 'wb') as wf: + wf.setnchannels(CHANNELS) + wf.setsampwidth(p.get_sample_size(FORMAT)) + wf.setframerate(RATE) + wf.writeframes(b''.join(chunks)) + chunks = [] + file_idx += 1 + + #print("received message: %s" % data) + stream.write(data) + except KeyboardInterrupt: + break + +stream.stop_stream() +stream.close() +p.terminate() + +sock.close() \ No newline at end of file diff --git a/tests/mic_streaming/run_test.py b/tests/mic_streaming/run_test.py new file mode 100644 index 00000000..33f5f254 --- /dev/null +++ b/tests/mic_streaming/run_test.py @@ -0,0 +1,90 @@ +import pyaudio +import socket +import wave +import os +from datetime import datetime + +""" +Listen on udp port 6055 for mic stream +""" +FORMAT = pyaudio.paInt16 # Format of sampling +CHANNELS = 1 # Number of audio channels (1 for mono, 2 for stereo) +RATE = 16000 # Sampling rate (samples per second) +CHUNK = 1024 # Number of audio frames per buffer +PORT = 6055 + +""" +Play and record wake-word-benchmark files +""" +ROOT_DIR = os.path.join( os.path.dirname(__file__), "../../") +BENCHMARK_DIR = os.path.join(ROOT_DIR, "testdata", "wake-word-benchmark" ) +TEST_FILES = [ + "audio/jarvis/0e165e17-134f-4cee-9ec6-b43d1412d7d7.wav", + "audio/jarvis/1c9fda58-050b-4b8f-8289-dd08c3107c8c.wav", + "audio/jarvis/6a8a0d5f-514c-4e7e-bc98-961ecef12f1f.wav" +] + +""" +Store records under test_runs/CURRENT_DATETIME +""" +TEST_RUN_ROOT = os.path.join(ROOT_DIR, "testdata", "mic_streaming" ) +TEST_RUN_DIR = os.path.join(TEST_RUN_ROOT, datetime.now().isoformat() ) +os.makedirs( TEST_RUN_DIR ) + + +# Create a UDP socket +sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) +sock.bind(("0.0.0.0", PORT)) + +# Create an audio object +p = pyaudio.PyAudio() + + +def play_wav_chunk(file_path): + chunk = 1024 + + # Open the WAV file + with wave.open(file_path, 'rb') as wf: + stream = p.open(format=p.get_format_from_width(wf.getsampwidth()), + channels=wf.getnchannels(), + rate=wf.getframerate(), + output=True) + print( f"WAV: rate:{wf.getframerate()} channels:{wf.getnchannels()}" ) + # Read data in chunks + data = wf.readframes(chunk) + + # yield audio chunk + while data: + stream.write(data) + yield data + data = wf.readframes(chunk) + + # Close and terminate the stream + stream.close() + + +def write_wav_file(file_path, chunks): + with wave.open( file_path, 'wb') as wf: + wf.setnchannels(CHANNELS) + wf.setsampwidth(p.get_sample_size(FORMAT)) + wf.setframerate(RATE) + wf.writeframes(b''.join(chunks)) + + +test_files = [ + os.path.join(BENCHMARK_DIR, test_file) for test_file in TEST_FILES +] + +for file_idx, test_file in enumerate(test_files): + chunks = [] + for chunk in play_wav_chunk(test_file) : + data, addr = sock.recvfrom(CHUNK) + chunks.append(data) + for i in range(40): + data, addr = sock.recvfrom(CHUNK) + chunks.append(data) + write_wav_file( os.path.join( TEST_RUN_DIR, f"rec_{file_idx:03d}.wav" ) , chunks) + + +sock.close() +p.terminate() \ No newline at end of file diff --git a/tests/mic_streaming/setup_testdata.sh b/tests/mic_streaming/setup_testdata.sh new file mode 100644 index 00000000..caa82a03 --- /dev/null +++ b/tests/mic_streaming/setup_testdata.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +GIT_ROOT=$(git rev-parse --show-toplevel) +BENSCHMARK_DIR=${GIT_ROOT}/testdata/wake-word-benchmark +TESTRUNS_ROOT=${GIT_ROOT}/testdata/mic_streaming + +if [ ! -d "${BENSCHMARK_DIR}" ]; then + cd ${GIT_ROOT}/testdata + git clone https://github.com/Picovoice/wake-word-benchmark.git +fi + +if [ ! -d "${TESTRUNS_ROOT}" ]; then + mkdir ${TESTRUNS_ROOT} +fi