mirror of https://github.com/arendst/Tasmota.git
commit
9048da1617
|
@ -0,0 +1,88 @@
|
||||||
|
# Run whenever a PR is generated or updated.
|
||||||
|
|
||||||
|
# Most jobs check out the code, ensure Python3 is installed, and for build
|
||||||
|
# tests the ESP8266 toolchain is cached when possible to speed up execution.
|
||||||
|
|
||||||
|
name: ESP8266Audio
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
build-esp8266:
|
||||||
|
name: Build ESP8266
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
chunk: [0, 1, 2, 3, 4]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
- uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: '3.x'
|
||||||
|
- name: Build Sketches
|
||||||
|
env:
|
||||||
|
TRAVIS_BUILD_DIR: ${{ github.workspace }}
|
||||||
|
TRAVIS_TAG: ${{ github.ref }}
|
||||||
|
BUILD_TYPE: build
|
||||||
|
BUILD_MOD: 5
|
||||||
|
BUILD_REM: ${{ matrix.chunk }}
|
||||||
|
run: |
|
||||||
|
bash ./tests/common.sh
|
||||||
|
|
||||||
|
|
||||||
|
build-esp32:
|
||||||
|
name: Build ESP-32
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
chunk: [0, 1, 2, 3, 4]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
- uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: '3.x'
|
||||||
|
- name: Build Sketches
|
||||||
|
env:
|
||||||
|
TRAVIS_BUILD_DIR: ${{ github.workspace }}
|
||||||
|
TRAVIS_TAG: ${{ github.ref }}
|
||||||
|
BUILD_TYPE: build_esp32
|
||||||
|
BUILD_MOD: 5
|
||||||
|
BUILD_REM: ${{ matrix.chunk }}
|
||||||
|
run: |
|
||||||
|
bash ./tests/common.sh
|
||||||
|
|
||||||
|
# Run host test suite under valgrind for runtime checking of code.
|
||||||
|
host-tests:
|
||||||
|
name: Host tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
- uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: '3.x'
|
||||||
|
- name: Run host tests
|
||||||
|
env:
|
||||||
|
TRAVIS_BUILD_DIR: ${{ github.workspace }}
|
||||||
|
TRAVIS_TAG: ${{ github.ref }}
|
||||||
|
run: |
|
||||||
|
sudo dpkg --add-architecture i386
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install valgrind lcov gcc-multilib g++-multilib libc6-dbg:i386
|
||||||
|
cd ./tests/host/
|
||||||
|
make
|
||||||
|
valgrind --leak-check=full --track-origins=yes -v --error-limit=no --show-leak-kinds=all --error-exitcode=999 ./mp3
|
||||||
|
valgrind --leak-check=full --track-origins=yes -v --error-limit=no --show-leak-kinds=all --error-exitcode=999 ./aac
|
||||||
|
valgrind --leak-check=full --track-origins=yes -v --error-limit=no --show-leak-kinds=all --error-exitcode=999 ./wav
|
||||||
|
valgrind --leak-check=full --track-origins=yes -v --error-limit=no --show-leak-kinds=all --error-exitcode=999 ./midi
|
||||||
|
|
|
@ -0,0 +1,674 @@
|
||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||||
|
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.
|
||||||
|
|
||||||
|
{one line to give the program's name and a brief idea of what it does.}
|
||||||
|
Copyright (C) {year} {name of author}
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
{project} Copyright (C) {year} {fullname}
|
||||||
|
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
|
||||||
|
<http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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
|
||||||
|
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
|
@ -0,0 +1,264 @@
|
||||||
|
# ESP8266Audio - supports ESP8266 & ESP32 [![Gitter](https://badges.gitter.im/ESP8266Audio/community.svg)](https://gitter.im/ESP8266Audio/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||||
|
Arduino library for parsing and decoding MOD, WAV, MP3, FLAC, MIDI, AAC, and RTTL files and playing them on an I2S DAC or even using a software-simulated delta-sigma DAC with dynamic 32x-128x oversampling.
|
||||||
|
|
||||||
|
ESP8266 is fully supported and most mature, but ESP32 is also mostly there with built-in DAC as well as external ones.
|
||||||
|
|
||||||
|
For real-time, autonomous speech synthesis, check out [ESP8266SAM](https://github.com/earlephilhower/ESP8266SAM), a library which uses this one and a port of an ancient format-based synthesis program to allow your ESP8266 to talk with low memory and no network required.
|
||||||
|
|
||||||
|
## Disclaimer
|
||||||
|
All this code is released under the GPL, and all of it is to be used at your own risk. If you find any bugs, please let me know via the GitHub issue tracker or drop me an email. The MOD and MP3 routines were taken from StellaPlayer and libMAD respectively. The software I2S delta-sigma 32x oversampling DAC was my own creation, and sounds quite good if I do say so myself.
|
||||||
|
|
||||||
|
The AAC decode code is from the Helix project and licensed under RealNetwork's RSPL license. For commercial use you're still going to need the usual AAC licensing from [Via Licensing](http://www.via-corp.com/us/en/licensing/aac/overview.html).
|
||||||
|
|
||||||
|
On the ESP32, AAC-SBR is supported (many webradio stations use this to reduce bandwidth even further). The ESP8266, however, does not support it due to a lack of onboard RAM.
|
||||||
|
|
||||||
|
MIDI decoding comes from a highly ported [MIDITONES](https://github.com/LenShustek/miditones) combined with a massively memory-optimized [TinySoundFont](https://github.com/schellingb/TinySoundFont), see the respective source files for more information.
|
||||||
|
|
||||||
|
Opus, OGG, and OpusFile are from [Xiph.org](https://xiph.org) with the Xiph license and patent described in src/{opusfile,libggg,libopus}/COPYING.. **NOTE** Opus decoding currently only works on the ESP32 due to the large memory requirements of opusfile. PRs to rewrite it to be less memory intensive would be much appreciated.
|
||||||
|
|
||||||
|
## Neat Things People Have Done With ESP8266Audio
|
||||||
|
If you have a neat use for this library, [I'd love to hear about it](mailto:earlephilhower@yahoo.com)!
|
||||||
|
|
||||||
|
My personal use of the ESP8266Audio library is only to drive a 3D-printed, network-time-setting alarm clock for my kids which can play an MP3 instead of a bell to wake them up, called [Psychoclock](https://github.com/earlephilhower/psychoclock).
|
||||||
|
|
||||||
|
Harald Sattler has built a neat German [word clock with MP3 alarm](http://www.harald-sattler.de/html/mini-wecker.htm). Detailed discussion on the process and models are included.
|
||||||
|
|
||||||
|
Erich Heinemann has developed a Stomper (instrument for playing samples in real-time during a live stage performance) that you can find more info about [here](https://github.com/ErichHeinemann/hman-stomper).
|
||||||
|
|
||||||
|
Dagnall53 has integrated this into a really neat MQTT based model train controller to add sounds to his set. More info is available [here](https://github.com/dagnall53/ESPMQTTRocnetSound), including STL files for 3D printed components!
|
||||||
|
|
||||||
|
JohannesMTC has built a similar project especially for model trains: https://github.com/JohannesMTC/ESP32_MAS
|
||||||
|
|
||||||
|
A neat MQTT-driven ESP8266 light-and-sound device (alarm? toy? who can say!) was built by @CosmicMac, available at https://github.com/CosmicMac/ESParkle
|
||||||
|
|
||||||
|
A very interesting "linear clock" with a stepper motor, NTP time keeping, and configurable recorded chimes with schematics, 3D printer plans, and source code, is now available http://home.kpn.nl/bderogee1980/projects/linear_clock/linear_clock.html
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
First, make sure you are running the 2.6.3/later or GIT head version of the Arduino libraries for ESP8266, or the latest ESP32 SDK from Espressif.
|
||||||
|
|
||||||
|
You can use GIT to pull right from GitHub: see [this README](https://github.com/esp8266/Arduino/blob/master/README.md#using-git-version) for detailed instructions.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
Install the library in your ~/Arduino/libraries
|
||||||
|
```sh
|
||||||
|
mkdir -p ~/Arduino/libraries
|
||||||
|
cd ~/Arduino/libraries
|
||||||
|
git clone https://github.com/earlephilhower/ESP8266Audio
|
||||||
|
```
|
||||||
|
|
||||||
|
When in the IDE please select the following options on the ESP8266:
|
||||||
|
```
|
||||||
|
Tools->lwIP Variant->v1.4 Open Source, or V2 Higher Bandwidth
|
||||||
|
Tools->CPU Frequency->160MHz
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
Create an AudioInputXXX source pointing to your input file, an AudioOutputXXX sink as either an I2S, I2S-sw-DAC, or as a "SerialWAV" which simply writes a WAV file to the Serial port which can be dumped to a file on your development system, and an AudioGeneratorXXX to actually take that input and decode it and send to the output.
|
||||||
|
|
||||||
|
After creation, you need to call the AudioGeneratorXXX::loop() routine from inside your own main loop() one or more times. This will automatically read as much of the file as needed and fill up the I2S buffers and immediately return. Since this is not interrupt driven, if you have large delay()s in your code, you may end up with hiccups in playback. Either break large delays into very small ones with calls to AudioGenerator::loop(), or reduce the sampling rate to require fewer samples per second.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
See the examples directory for some simple examples, but the following snippet can play an MP3 file over the simulated I2S DAC:
|
||||||
|
```cpp
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include "AudioFileSourceSPIFFS.h"
|
||||||
|
#include "AudioGeneratorMP3.h"
|
||||||
|
#include "AudioOutputI2SNoDAC.h"
|
||||||
|
|
||||||
|
AudioGeneratorMP3 *mp3;
|
||||||
|
AudioFileSourceSPIFFS *file;
|
||||||
|
AudioOutputI2SNoDAC *out;
|
||||||
|
void setup()
|
||||||
|
{
|
||||||
|
Serial.begin(115200);
|
||||||
|
delay(1000);
|
||||||
|
SPIFFS.begin();
|
||||||
|
file = new AudioFileSourceSPIFFS("/jamonit.mp3");
|
||||||
|
out = new AudioOutputI2SNoDAC();
|
||||||
|
mp3 = new AudioGeneratorMP3();
|
||||||
|
mp3->begin(file, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop()
|
||||||
|
{
|
||||||
|
if (mp3->isRunning()) {
|
||||||
|
if (!mp3->loop()) mp3->stop();
|
||||||
|
} else {
|
||||||
|
Serial.printf("MP3 done\n");
|
||||||
|
delay(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## AudioFileSource classes
|
||||||
|
AudioFileSource: Base class which implements a very simple read-only "file" interface. Required because it seems everyone has invented their own filesystem on the Arduino with their own unique twist. Using this wrapper lets that be abstracted and makes the AudioGenerator simpler as it only calls these simple functions.
|
||||||
|
|
||||||
|
AudioFileSourceSPIFFS: Reads a file from the SPIFFS filesystem
|
||||||
|
|
||||||
|
AudioFileSourcePROGMEM: Reads a file from a PROGMEM array. Under UNIX you can use "xxd -i file.mp3 > file.h" to get the basic format, then add "const" and "PROGMEM" to the generated array and include it in your sketch. See the example .h files for a concrete example.
|
||||||
|
|
||||||
|
AudioFileSourceHTTPStream: Simple implementation of a streaming HTTP reader for ShoutCast-type MP3 streaming. Not yet resilient, and at 44.1khz 128bit stutters due to CPU limitations, but it works more or less.
|
||||||
|
|
||||||
|
## AudioFileSourceBuffer - Double buffering, useful for HTTP streams
|
||||||
|
AudioFileSourceBuffer is an input source that simpy adds an additional RAM buffer of the output of any other AudioFileSource. This is particularly useful for web streaming where you need to have 1-2 packets in memory to ensure hiccup-free playback.
|
||||||
|
|
||||||
|
Create your standard input file source, create the buffer with the original source as its input, and pass this buffer object to the generator.
|
||||||
|
```cpp
|
||||||
|
...
|
||||||
|
AudioGeneratorMP3 *mp3;
|
||||||
|
AudioFileSourceHTTPStream *file;
|
||||||
|
AudioFileSourceBuffer *buff;
|
||||||
|
AudioOutputI2SNoDAC *out;
|
||||||
|
...
|
||||||
|
// Create the HTTP stream normally
|
||||||
|
file = new AudioFileSourceHTTPStream("http://your.url.here/mp3");
|
||||||
|
// Create a buffer using that stream
|
||||||
|
buff = new AudioFileSourceBuffer(file, 2048);
|
||||||
|
out = new AudioOutputI2SNoDAC();
|
||||||
|
mp3 = new AudioGeneratorMP3();
|
||||||
|
// Pass in the *buffer*, not the *http stream* to enable buffering
|
||||||
|
mp3->begin(buff, out);
|
||||||
|
...
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## AudioFileSourceID3 - ID3 stream parser filter with a user-specified callback
|
||||||
|
This class, which takes as input any other AudioFileSource and outputs an AudioFileSource suitable for any decoder, automatically parses out ID3 tags from MP3 files. You need to specify a callback function, which will be called as tags are decoded and allow you to update your UI state with this information. See the PlayMP3FromSPIFFS example for more information.
|
||||||
|
|
||||||
|
## AudioGenerator classes
|
||||||
|
AudioGenerator: Base class for all file decoders. Takes a AudioFileSource and an AudioOutput object to get the data from and to write decoded samples to. Call its loop() function as often as you can to ensure the buffers are always kept full and your music won't skip.
|
||||||
|
|
||||||
|
AudioGeneratorWAV: Reads and plays Microsoft WAVE (.WAV) format files of 8 or 16 bits.
|
||||||
|
|
||||||
|
AudioGeneratorMOD: Reads and plays Amiga ModTracker files (.MOD). Use a 160MHz clock as this requires tons of SPIFFS reads (which are painfully slow) to get raw instrument sample data for every output sample. See https://modarchive.org for many free MOD files.
|
||||||
|
|
||||||
|
AudioGeneratorMP3: Reads and plays MP3 format files (.MP3) using a ported libMAD library. Use a 160MHz clock to ensure enough compute power to decode 128KBit 44.1KHz without hiccups. For complete porting history with the gory details, look at https://github.com/earlephilhower/libmad-8266
|
||||||
|
|
||||||
|
AudioGeneratorFLAC: Plays FLAC files via ported libflac-1.3.2. On the order of 30KB heap and minimal stack required as-is.
|
||||||
|
|
||||||
|
AudioGeneratorMIDI: Plays a MIDI file using a wavetable synthesizer and a SoundFont2 wavetable input. Theoretically up to 16 simultaneous notes available, but depending on the memory needed for the SF2 structures you may not be able to get that many before hitting OOM.
|
||||||
|
|
||||||
|
AudioGeneratorAAC: Requires about 30KB of heap and plays a mono or stereo AAC file using the Helix fixed-point AAC decoder.
|
||||||
|
|
||||||
|
AudioGeneratorRTTTL: Enjoy the pleasures of monophonic, 4-octave ringtones on your ESP8266. Very low memory and CPU requirements for simple tunes.
|
||||||
|
|
||||||
|
## AudioOutput classes
|
||||||
|
AudioOutput: Base class for all output drivers. Takes a sample at a time and returns true/false if there is buffer space for it. If it returns false, it is the calling object's (AudioGenerator's) job to keep the data that didn't fit and try again later.
|
||||||
|
|
||||||
|
AudioOutputI2S: Interface for any I2S 16-bit DAC. Sends stereo or mono signals out at whatever frequency set. Tested with Adafruit's I2SDAC and a Beyond9032 DAC from eBay. Tested up to 44.1KHz. To use the internal DAC on ESP32, instantiate this class as `AudioOutputI2S(0,1)`, see example `PlayMODFromPROGMEMToDAC` and code in [AudioOutputI2S.cpp](src/AudioOutputI2S.cpp#L29) for details.
|
||||||
|
|
||||||
|
AudioOutputI2SNoDAC: Abuses the I2S interface to play music without a DAC. Turns it into a 32x (or higher) oversampling delta-sigma DAC. Use the schematic below to drive a speaker or headphone from the I2STx pin (i.e. Rx). Note that with this interface, depending on the transistor used, you may need to disconnect the Rx pin from the driver to perform serial uploads. Mono-only output, of course.
|
||||||
|
|
||||||
|
AudioOutputSPDIF (experimental): Another way to abuse the I2S peripheral to send out BMC encoded S/PDIF bitstream. To interface with S/PDIF receiver it needs optical or coaxial transceiver, for which some examples can be found at https://www.epanorama.net/documents/audio/spdif.html. It should work even with the simplest form with red LED and current limiting resistor, fed into TOSLINK cable. Minimum sample rate supported by is 32KHz. Due to BMC coding, actual symbol rate on the pin is 4x normal I2S data rate, which drains DMA buffers quickly. See more details inside [AudioOutputSPDIF.cpp](src/AudioOutputSPDIF.cpp#L17)
|
||||||
|
|
||||||
|
AudioOutputSerialWAV: Writes a binary WAV format with headers to the Serial port. If you capture the serial output to a file you can play it back on your development system.
|
||||||
|
|
||||||
|
AudioOutputSPIFFSWAV: Writes a binary WAV format with headers to a SPIFFS filesystem. Ensure the FS is mounted and SPIFFS is started before calling. USe the SetFilename() call to pick the output file before starting.
|
||||||
|
|
||||||
|
AudioOutputNull: Just dumps samples to /dev/null. Used for speed testing as it doesn't artificially limit the AudioGenerator output speed since there are no buffers to fill/drain.
|
||||||
|
|
||||||
|
## I2S DACs
|
||||||
|
I've used both the Adafruit [I2S +3W amp DAC](https://www.adafruit.com/product/3006) and a generic PCM5102 based DAC with success. The biggest problems I've seen from users involve pinouts from the ESP8266 for GPIO and hooking up all necessary pins on the DAC board.
|
||||||
|
|
||||||
|
### Adafruit I2S DAC
|
||||||
|
This is quite simple and only needs the GND, VIN, LRC, BCLK< and DIN pins to be wired. Be sure to use +5V on the VIN to get the loudest sound. See the [Adafruit example page](https://learn.adafruit.com/adafruit-max98357-i2s-class-d-mono-amp) for more info.
|
||||||
|
|
||||||
|
### PCM5102 DAC
|
||||||
|
I've used several versions of PCM5102 DAC boards purchased from eBay. They've all had the same pinout, no matter the form factor. There are several input configuration pins beyond the I2S interface itself that need to be wired:
|
||||||
|
* 3.3V from ESP8266 -> VCC, 33V, XMT
|
||||||
|
* GND from ESP8266 -> GND, FLT, DMP, FMT, SCL
|
||||||
|
* (Standard I2S interface) BCLK->BCK, I2SO->DIN, and LRCLK(WS)->LCK
|
||||||
|
|
||||||
|
|
||||||
|
### Others
|
||||||
|
There are many other variants out there, and they should all work reasonably well with this code and the ESP8266. Please be certain you've read the datasheet and are applying proper input voltages, and be sure to tie off any unused inputs to GND or VCC as appropriate. LEaving an input pin floating on any integrated circuit can cause unstable operation as it may pick up noise from the environment (very low input capacitance) and cause havoc with internal IC settings.
|
||||||
|
|
||||||
|
## Software I2S Delta-Sigma DAC (i.e. playing music with a single transistor and speaker)
|
||||||
|
For the best fidelity, and stereo to boot, spend the money on a real I2S DAC. Adafruit makes a great mono one with amplifier, and you can find stereo unamplified ones on eBay or elsewhere quite cheaply. However, thanks to the software delta-sigma DAC with 32x oversampling (up to 128x if the audio rate is low enough) you can still have pretty good sound!
|
||||||
|
|
||||||
|
Use the `AudioOutputI2S*No*DAC` object instead of the `AudioOutputI2S` in your code, and the following schematic to drive a 2-3W speaker using a single $0.05 NPN 2N3904 transistor and ~1K resistor:
|
||||||
|
|
||||||
|
```
|
||||||
|
2N3904 (NPN)
|
||||||
|
+---------+
|
||||||
|
| | +-|
|
||||||
|
| E B C | / S|
|
||||||
|
+-|--|--|-+ | P|
|
||||||
|
| | +------+ E|
|
||||||
|
| | | A|
|
||||||
|
ESP8266-GND ------------------+ | +------+ K|
|
||||||
|
| | | E|
|
||||||
|
ESP8266-I2SOUT (Rx) -----/\/\/\--+ | \ R|
|
||||||
|
| +-|
|
||||||
|
USB 5V -----------------------------+
|
||||||
|
|
||||||
|
You may also want to add a 220uF cap from USB5V to GND just to help filter out any voltage droop during high volume playback.
|
||||||
|
```
|
||||||
|
If you don't have a 5V source available on your ESP model, you can use the 5V from your USB serial adapter, or even the 3V from the ESP8266 (but it'll be lower volume). Don't try and drive the speaker without the transistor, the ESP8266 pins can't give enough current to drive even a headphone well and you may end up damaging your device.
|
||||||
|
|
||||||
|
Connections are as a follows:
|
||||||
|
```
|
||||||
|
ESP8266-RX(I2S tx) -- Resistor (~1K ohm, not critical) -- 2N3904 Base
|
||||||
|
ESP8266-GND -- 2N3904 Emitter
|
||||||
|
USB-5V -- Speaker + Terminal
|
||||||
|
2N3904-Collector -- Speaker - Terminal
|
||||||
|
```
|
||||||
|
|
||||||
|
*NOTE*: A prior version of this schematic had a direct connection from the ESP8266 to the base of the transistor. While this does provide the maximum amplitude, it also can draw more current from the 8266 than is safe, and can also cause the transistor to overheat.
|
||||||
|
|
||||||
|
As of the latest ESP8266Audio release, with the software delta-sigma DAC the LRCLK and BCLK pins *can* be used by an application. Simply use normal `pinMode` and `dicitalWrite` or `digitalRead` as desired.
|
||||||
|
|
||||||
|
### High pitched buzzing with the 1-T circuit
|
||||||
|
The 1-T amp can _NOT_ drive any sort of amplified speaker. If there is a power or USB input to the speaker, or it has lights or Bluetooth or a battery, it can _NOT_ be used with this circuit.
|
||||||
|
|
||||||
|
The 1T output is a binary signal at 0 or 5V, with nothing in between. When you connect to a 8ohm paper physical speaker directly, the speaker cone itself has inertia and acts as a low pass filter and averages the density of pulses in order to give a nice, analog output.
|
||||||
|
|
||||||
|
When you feed the 1T output to an amp you are alternatively grounding and overdriving the op-amp's input at a high frequency. That causes ringing and the opamp has a frequency response high enough to amplify the high frequency noise and you get that buzzing.
|
||||||
|
|
||||||
|
The same problem may happen with piezo speakers. They have a very high frequency response, normally, and have (almost) no inertia. So you hear the buzzing at high frequency.
|
||||||
|
|
||||||
|
You could attach the 1T output to a low pass and feed that into an amplifier. But at that point it is easier to just get an I2S DAC and avoid the whole thing (plus get stereo and true 16-bit output).
|
||||||
|
|
||||||
|
### Debugging the 1-T amp circuit, compliments of @msmcmickey
|
||||||
|
If you've built the amp but are not getting any sound, @msmcmickey wrote up a very good debugging sequence to check:
|
||||||
|
|
||||||
|
0. Please double-check the wiring. GPIO pins and board pins are not always the same and vary immensely between brands of ESP8266 carrier boards.
|
||||||
|
1. Is the transistor connected properly? Check the datasheet for this package style and make sure you have the leads connected properly. This package has three leads, and the lead that is by itself in the middle of the one side is the collector, not the base as you might expect it to be.
|
||||||
|
2. If connected properly, do you have ~5 volts between the collector and emitter?
|
||||||
|
3. Was the transistor possibly damaged/overheated during soldering, or by connecting it improperly? Out-of-circuit diode check voltage drop test using a multimeter from base->emitter and base->collector should be between .5 and .7 volts. If it's shorted or open or conducting in both directions, then replace it and make sure it's connected properly.
|
||||||
|
|
||||||
|
## SPDIF optical output
|
||||||
|
The proper way would be using optical TOSLINK transmitter (i.e. TOTXxxx). For testing, you can try with ~660nm red LED and resistor. Same as your basic Blink project with external LED, just that the LED will blink a bit faster.
|
||||||
|
```
|
||||||
|
____
|
||||||
|
ESP Pin -------|____|--------+
|
||||||
|
|
|
||||||
|
---
|
||||||
|
V LED
|
||||||
|
---
|
||||||
|
|
|
||||||
|
Ground ---------------------+
|
||||||
|
```
|
||||||
|
For ESP8266 with red LED (~1.9Vf drop) you need minimum 150Ohm resistor (12mA max per pin), and output pin is fixed (GPIO3/RX0).On ESP32 it is confgurable with `AudioOutputSPDIF(gpio_num)`.
|
||||||
|
|
||||||
|
## Using external SPI RAM to increase buffer
|
||||||
|
A class allows you to use a 23lc1024 SPI RAM from Microchip as input buffer. This chip connects to ESP8266 HSPI port and provides a large buffer to help avoid hiccus in playback of web streams.
|
||||||
|
|
||||||
|
The current version allows for using the standard hardware CS (GPIO15) or any other pin via software at slightly less performance. The following schematic shows one example:
|
||||||
|
|
||||||
|
![Example of SPIRAM Schematic](examples/StreamMP3FromHTTP_SPIRAM/Schema_Spiram.png)
|
||||||
|
|
||||||
|
## Notes for using SD cards and ESP8266Audio on Wemos shields
|
||||||
|
I've been told the Wemos SD card shield uses GPIO15 as the SD chip select. This needs to be changed because GPIO15 == I2SBCLK, and is driven even if you're using the NoDAC option. Once you move the CS to another pin and update your program it should work fine.
|
||||||
|
|
||||||
|
## Porting to other microcontrollers
|
||||||
|
There's no ESP8266-specific code in the AudioGenerator routines, so porting to other controllers should be relatively easy assuming they have the same endianness as the Xtensa core used. Drop me a line if you're doing this, I may be able to help point you in the right direction.
|
||||||
|
|
||||||
|
## Thanks
|
||||||
|
Thanks to the authors of StellaPlayer and libMAD for releasing their code freely, and to the maintainers and contributors to the ESP8266 Arduino port.
|
||||||
|
|
||||||
|
Also, big thanks to @tueddy for getting the initial ESP32 porting into the tree!
|
||||||
|
|
||||||
|
-Earle F. Philhower, III
|
||||||
|
earlephilhower@yahoo.com
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
#include <Arduino.h>
|
||||||
|
#ifdef ESP32
|
||||||
|
#include <WiFi.h>
|
||||||
|
#else
|
||||||
|
#include <ESP8266WiFi.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "AudioFileSourcePROGMEM.h"
|
||||||
|
#include "AudioGeneratorWAV.h"
|
||||||
|
#include "AudioOutputI2S.h"
|
||||||
|
#include "AudioOutputMixer.h"
|
||||||
|
|
||||||
|
// VIOLA sample taken from https://ccrma.stanford.edu/~jos/pasp/Sound_Examples.html
|
||||||
|
#include "viola.h"
|
||||||
|
|
||||||
|
AudioGeneratorWAV *wav[2];
|
||||||
|
AudioFileSourcePROGMEM *file[2];
|
||||||
|
AudioOutputI2S *out;
|
||||||
|
AudioOutputMixer *mixer;
|
||||||
|
AudioOutputMixerStub *stub[2];
|
||||||
|
|
||||||
|
void setup()
|
||||||
|
{
|
||||||
|
WiFi.mode(WIFI_OFF);
|
||||||
|
Serial.begin(115200);
|
||||||
|
delay(1000);
|
||||||
|
Serial.printf("WAV start\n");
|
||||||
|
|
||||||
|
audioLogger = &Serial;
|
||||||
|
file[0] = new AudioFileSourcePROGMEM( viola, sizeof(viola) );
|
||||||
|
out = new AudioOutputI2S();
|
||||||
|
mixer = new AudioOutputMixer(32, out);
|
||||||
|
stub[0] = mixer->NewInput();
|
||||||
|
stub[0]->SetGain(0.3);
|
||||||
|
wav[0] = new AudioGeneratorWAV();
|
||||||
|
wav[0]->begin(file[0], stub[0]);
|
||||||
|
// Begin wav[1] later in loop()
|
||||||
|
Serial.printf("starting 1\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop()
|
||||||
|
{
|
||||||
|
static uint32_t start = 0;
|
||||||
|
static bool go = false;
|
||||||
|
|
||||||
|
if (!start) start = millis();
|
||||||
|
|
||||||
|
if (wav[0]->isRunning()) {
|
||||||
|
if (!wav[0]->loop()) { wav[0]->stop(); stub[0]->stop(); Serial.printf("stopping 1\n"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (millis()-start > 3000) {
|
||||||
|
if (!go) {
|
||||||
|
Serial.printf("starting 2\n");
|
||||||
|
stub[1] = mixer->NewInput();
|
||||||
|
stub[1]->SetGain(0.4);
|
||||||
|
wav[1] = new AudioGeneratorWAV();
|
||||||
|
file[1] = new AudioFileSourcePROGMEM( viola, sizeof(viola) );
|
||||||
|
wav[1]->begin(file[1], stub[1]);
|
||||||
|
go = true;
|
||||||
|
}
|
||||||
|
if (wav[1]->isRunning()) {
|
||||||
|
if (!wav[1]->loop()) { wav[1]->stop(); stub[1]->stop(); Serial.printf("stopping 2\n");}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,33 @@
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include "AudioGeneratorAAC.h"
|
||||||
|
#include "AudioOutputI2S.h"
|
||||||
|
#include "AudioFileSourcePROGMEM.h"
|
||||||
|
#include "sampleaac.h"
|
||||||
|
|
||||||
|
AudioFileSourcePROGMEM *in;
|
||||||
|
AudioGeneratorAAC *aac;
|
||||||
|
AudioOutputI2S *out;
|
||||||
|
|
||||||
|
void setup()
|
||||||
|
{
|
||||||
|
Serial.begin(115200);
|
||||||
|
|
||||||
|
audioLogger = &Serial;
|
||||||
|
in = new AudioFileSourcePROGMEM(sampleaac, sizeof(sampleaac));
|
||||||
|
aac = new AudioGeneratorAAC();
|
||||||
|
out = new AudioOutputI2S();
|
||||||
|
|
||||||
|
aac->begin(in, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void loop()
|
||||||
|
{
|
||||||
|
if (aac->isRunning()) {
|
||||||
|
aac->loop();
|
||||||
|
} else {
|
||||||
|
Serial.printf("AAC done\n");
|
||||||
|
delay(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,68 @@
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include "AudioFileSourceSD.h"
|
||||||
|
#include "AudioOutputSPDIF.h"
|
||||||
|
#include "AudioGeneratorFLAC.h"
|
||||||
|
|
||||||
|
// For this sketch, you need connected SD card with '.flac' music files in the root
|
||||||
|
// directory. Some samples with various sampling rates are available from i.e.
|
||||||
|
// Espressif Audio Development Framework at:
|
||||||
|
// https://docs.espressif.com/projects/esp-adf/en/latest/design-guide/audio-samples.html
|
||||||
|
//
|
||||||
|
// On ESP8266 you might need to reencode FLAC files with max '-2' compression level
|
||||||
|
// (i.e. 1152 maximum block size) or you will run out of memory. FLAC files will be
|
||||||
|
// slightly bigger but you don't loose audio quality with reencoding (lossles codec).
|
||||||
|
|
||||||
|
// You may need a fast SD card. Set this as high as it will work (40MHz max).
|
||||||
|
#define SPI_SPEED SD_SCK_MHZ(40)
|
||||||
|
|
||||||
|
// On ESP32 you can adjust the SPDIF_OUT_PIN (GPIO number).
|
||||||
|
// On ESP8266 it is fixed to GPIO3/RX0 and this setting has no effect
|
||||||
|
#define SPDIF_OUT_PIN 27
|
||||||
|
|
||||||
|
File dir;
|
||||||
|
AudioFileSourceSD *source = NULL;
|
||||||
|
AudioOutputSPDIF *output = NULL;
|
||||||
|
AudioGeneratorFLAC *decoder = NULL;
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
Serial.begin(115200);
|
||||||
|
Serial.println();
|
||||||
|
delay(1000);
|
||||||
|
|
||||||
|
audioLogger = &Serial;
|
||||||
|
source = new AudioFileSourceSD();
|
||||||
|
output = new AudioOutputSPDIF(SPDIF_OUT_PIN);
|
||||||
|
decoder = new AudioGeneratorFLAC();
|
||||||
|
|
||||||
|
// NOTE: SD.begin(...) should be called AFTER AudioOutputSPDIF()
|
||||||
|
// to takover the the SPI pins if they share some with I2S
|
||||||
|
// (i.e. D8 on Wemos D1 mini is both I2S BCK and SPI SS)
|
||||||
|
#if defined(ESP8266)
|
||||||
|
SD.begin(SS, SPI_SPEED);
|
||||||
|
#else
|
||||||
|
SD.begin();
|
||||||
|
#endif
|
||||||
|
dir = SD.open("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
if ((decoder) && (decoder->isRunning())) {
|
||||||
|
if (!decoder->loop()) decoder->stop();
|
||||||
|
} else {
|
||||||
|
File file = dir.openNextFile();
|
||||||
|
if (file) {
|
||||||
|
if (String(file.name()).endsWith(".flac")) {
|
||||||
|
source->close();
|
||||||
|
if (source->open(file.name())) {
|
||||||
|
Serial.printf_P(PSTR("Playing '%s' from SD card...\n"), file.name());
|
||||||
|
decoder->begin(source, output);
|
||||||
|
} else {
|
||||||
|
Serial.printf_P(PSTR("Error opening '%s'\n"), file.name());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Serial.println(F("Playback form SD card done\n"));
|
||||||
|
delay(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <AudioOutputI2S.h>
|
||||||
|
#include <AudioFileSourcePROGMEM.h>
|
||||||
|
#include <AudioGeneratorFLAC.h>
|
||||||
|
|
||||||
|
#include "sample.h"
|
||||||
|
|
||||||
|
AudioOutputI2S *out;
|
||||||
|
AudioFileSourcePROGMEM *file;
|
||||||
|
AudioGeneratorFLAC *flac;
|
||||||
|
|
||||||
|
void setup()
|
||||||
|
{
|
||||||
|
Serial.begin(115200);
|
||||||
|
Serial.println("Starting up...\n");
|
||||||
|
|
||||||
|
audioLogger = &Serial;
|
||||||
|
file = new AudioFileSourcePROGMEM( sample_flac, sizeof(sample_flac) );
|
||||||
|
out = new AudioOutputI2S();
|
||||||
|
flac = new AudioGeneratorFLAC();
|
||||||
|
flac->begin(file, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop()
|
||||||
|
{
|
||||||
|
if (flac->isRunning()) {
|
||||||
|
if (!flac->loop()) flac->stop();
|
||||||
|
} else {
|
||||||
|
Serial.printf("FLAC done\n");
|
||||||
|
delay(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,54 @@
|
||||||
|
#include <Arduino.h>
|
||||||
|
#ifdef ESP32
|
||||||
|
void setup() {
|
||||||
|
Serial.begin(115200);
|
||||||
|
Serial.printf("ERROR - ESP32 does not support LittleFS\n");
|
||||||
|
}
|
||||||
|
void loop() {}
|
||||||
|
#else
|
||||||
|
#include <ESP8266WiFi.h>
|
||||||
|
#include <AudioOutputI2S.h>
|
||||||
|
#include <AudioGeneratorMIDI.h>
|
||||||
|
#include <AudioFileSourceLittleFS.h>
|
||||||
|
|
||||||
|
AudioFileSourceLittleFS *sf2;
|
||||||
|
AudioFileSourceLittleFS *mid;
|
||||||
|
AudioOutputI2S *dac;
|
||||||
|
AudioGeneratorMIDI *midi;
|
||||||
|
|
||||||
|
void setup()
|
||||||
|
{
|
||||||
|
const char *soundfont = "/1mgm.sf2";
|
||||||
|
const char *midifile = "/furelise.mid";
|
||||||
|
|
||||||
|
WiFi.mode(WIFI_OFF);
|
||||||
|
|
||||||
|
Serial.begin(115200);
|
||||||
|
Serial.println("Starting up...\n");
|
||||||
|
|
||||||
|
audioLogger = &Serial;
|
||||||
|
sf2 = new AudioFileSourceLittleFS(soundfont);
|
||||||
|
mid = new AudioFileSourceLittleFS(midifile);
|
||||||
|
|
||||||
|
dac = new AudioOutputI2S();
|
||||||
|
midi = new AudioGeneratorMIDI();
|
||||||
|
midi->SetSoundfont(sf2);
|
||||||
|
midi->SetSampleRate(22050);
|
||||||
|
Serial.printf("BEGIN...\n");
|
||||||
|
midi->begin(mid, dac);
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop()
|
||||||
|
{
|
||||||
|
if (midi->isRunning()) {
|
||||||
|
if (!midi->loop()) {
|
||||||
|
uint32_t e = millis();
|
||||||
|
midi->stop();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Serial.printf("MIDI done\n");
|
||||||
|
delay(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,55 @@
|
||||||
|
#include <Arduino.h>
|
||||||
|
#ifdef ESP32
|
||||||
|
#include <WiFi.h>
|
||||||
|
#include "SPIFFS.h"
|
||||||
|
#else
|
||||||
|
#include <ESP8266WiFi.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
#include <AudioOutputNull.h>
|
||||||
|
#include <AudioOutputI2S.h>
|
||||||
|
#include <AudioGeneratorMIDI.h>
|
||||||
|
#include <AudioFileSourceSPIFFS.h>
|
||||||
|
|
||||||
|
AudioFileSourceSPIFFS *sf2;
|
||||||
|
AudioFileSourceSPIFFS *mid;
|
||||||
|
AudioOutputI2S *dac;
|
||||||
|
AudioGeneratorMIDI *midi;
|
||||||
|
|
||||||
|
void setup()
|
||||||
|
{
|
||||||
|
const char *soundfont = "/1mgm.sf2";
|
||||||
|
const char *midifile = "/furelise.mid";
|
||||||
|
|
||||||
|
WiFi.mode(WIFI_OFF);
|
||||||
|
|
||||||
|
Serial.begin(115200);
|
||||||
|
Serial.println("Starting up...\n");
|
||||||
|
|
||||||
|
audioLogger = &Serial;
|
||||||
|
sf2 = new AudioFileSourceSPIFFS(soundfont);
|
||||||
|
mid = new AudioFileSourceSPIFFS(midifile);
|
||||||
|
|
||||||
|
dac = new AudioOutputI2S();
|
||||||
|
midi = new AudioGeneratorMIDI();
|
||||||
|
midi->SetSoundfont(sf2);
|
||||||
|
midi->SetSampleRate(22050);
|
||||||
|
Serial.printf("BEGIN...\n");
|
||||||
|
midi->begin(mid, dac);
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop()
|
||||||
|
{
|
||||||
|
if (midi->isRunning()) {
|
||||||
|
if (!midi->loop()) {
|
||||||
|
uint32_t e = millis();
|
||||||
|
midi->stop();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Serial.printf("MIDI done\n");
|
||||||
|
delay(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,44 @@
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include "AudioFileSourcePROGMEM.h"
|
||||||
|
#include "AudioGeneratorMOD.h"
|
||||||
|
#include "AudioOutputI2S.h"
|
||||||
|
#ifdef ESP32
|
||||||
|
#include <WiFi.h>
|
||||||
|
#else
|
||||||
|
#include <ESP8266WiFi.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// enigma.mod sample from the mod archive: https://modarchive.org/index.php?request=view_by_moduleid&query=42146
|
||||||
|
#include "enigma.h"
|
||||||
|
|
||||||
|
AudioGeneratorMOD *mod;
|
||||||
|
AudioFileSourcePROGMEM *file;
|
||||||
|
AudioOutputI2S *out;
|
||||||
|
|
||||||
|
void setup()
|
||||||
|
{
|
||||||
|
WiFi.mode(WIFI_OFF); //WiFi.forceSleepBegin();
|
||||||
|
Serial.begin(115200);
|
||||||
|
delay(1000);
|
||||||
|
|
||||||
|
audioLogger = &Serial;
|
||||||
|
file = new AudioFileSourcePROGMEM( enigma_mod, sizeof(enigma_mod) );
|
||||||
|
// out = new AudioOutputI2S(0, 1); Uncomment this line, comment the next one to use the internal DAC channel 1 (pin25) on ESP32
|
||||||
|
out = new AudioOutputI2S();
|
||||||
|
mod = new AudioGeneratorMOD();
|
||||||
|
mod->SetBufferSize(3*1024);
|
||||||
|
mod->SetSampleRate(44100);
|
||||||
|
mod->SetStereoSeparation(32);
|
||||||
|
mod->begin(file, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop()
|
||||||
|
{
|
||||||
|
if (mod->isRunning()) {
|
||||||
|
if (!mod->loop()) mod->stop();
|
||||||
|
} else {
|
||||||
|
Serial.printf("MOD done\n");
|
||||||
|
delay(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,72 @@
|
||||||
|
#include <Arduino.h>
|
||||||
|
#ifdef ESP32
|
||||||
|
#include <WiFi.h>
|
||||||
|
#include "SPIFFS.h"
|
||||||
|
#else
|
||||||
|
#include <ESP8266WiFi.h>
|
||||||
|
#endif
|
||||||
|
#include "AudioFileSourceSPIFFS.h"
|
||||||
|
#include "AudioFileSourceID3.h"
|
||||||
|
#include "AudioGeneratorMP3.h"
|
||||||
|
#include "AudioOutputI2SNoDAC.h"
|
||||||
|
|
||||||
|
// To run, set your ESP8266 build to 160MHz, and include a SPIFFS of 512KB or greater.
|
||||||
|
// Use the "Tools->ESP8266/ESP32 Sketch Data Upload" menu to write the MP3 to SPIFFS
|
||||||
|
// Then upload the sketch normally.
|
||||||
|
|
||||||
|
// pno_cs from https://ccrma.stanford.edu/~jos/pasp/Sound_Examples.html
|
||||||
|
|
||||||
|
AudioGeneratorMP3 *mp3;
|
||||||
|
AudioFileSourceSPIFFS *file;
|
||||||
|
AudioOutputI2SNoDAC *out;
|
||||||
|
AudioFileSourceID3 *id3;
|
||||||
|
|
||||||
|
|
||||||
|
// Called when a metadata event occurs (i.e. an ID3 tag, an ICY block, etc.
|
||||||
|
void MDCallback(void *cbData, const char *type, bool isUnicode, const char *string)
|
||||||
|
{
|
||||||
|
(void)cbData;
|
||||||
|
Serial.printf("ID3 callback for: %s = '", type);
|
||||||
|
|
||||||
|
if (isUnicode) {
|
||||||
|
string += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (*string) {
|
||||||
|
char a = *(string++);
|
||||||
|
if (isUnicode) {
|
||||||
|
string++;
|
||||||
|
}
|
||||||
|
Serial.printf("%c", a);
|
||||||
|
}
|
||||||
|
Serial.printf("'\n");
|
||||||
|
Serial.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void setup()
|
||||||
|
{
|
||||||
|
WiFi.mode(WIFI_OFF);
|
||||||
|
Serial.begin(115200);
|
||||||
|
delay(1000);
|
||||||
|
SPIFFS.begin();
|
||||||
|
Serial.printf("Sample MP3 playback begins...\n");
|
||||||
|
|
||||||
|
audioLogger = &Serial;
|
||||||
|
file = new AudioFileSourceSPIFFS("/pno-cs.mp3");
|
||||||
|
id3 = new AudioFileSourceID3(file);
|
||||||
|
id3->RegisterMetadataCB(MDCallback, (void*)"ID3TAG");
|
||||||
|
out = new AudioOutputI2SNoDAC();
|
||||||
|
mp3 = new AudioGeneratorMP3();
|
||||||
|
mp3->begin(id3, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop()
|
||||||
|
{
|
||||||
|
if (mp3->isRunning()) {
|
||||||
|
if (!mp3->loop()) mp3->stop();
|
||||||
|
} else {
|
||||||
|
Serial.printf("MP3 done\n");
|
||||||
|
delay(1000);
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
|
@ -0,0 +1,99 @@
|
||||||
|
#include <Arduino.h>
|
||||||
|
#ifdef ESP32
|
||||||
|
#include "SPIFFS.h"
|
||||||
|
#endif
|
||||||
|
#include "AudioFileSourceSPIFFS.h"
|
||||||
|
#include "AudioFileSourceID3.h"
|
||||||
|
#include "AudioOutputSPDIF.h"
|
||||||
|
#include "AudioGeneratorMP3.h"
|
||||||
|
|
||||||
|
// To run, set your ESP8266 build to 160MHz, and include a SPIFFS partition
|
||||||
|
// big enough to hold your MP3 file. Find suitable MP3 file from i.e.
|
||||||
|
// https://docs.espressif.com/projects/esp-adf/en/latest/design-guide/audio-samples.html
|
||||||
|
// and download it into 'data' directory. Use the "Tools->ESP8266/ESP32 Sketch Data Upload"
|
||||||
|
// menu to write the MP3 to SPIFFS. Then upload the sketch normally.
|
||||||
|
|
||||||
|
AudioFileSourceSPIFFS *file;
|
||||||
|
AudioFileSourceID3 *id3;
|
||||||
|
AudioOutputSPDIF *out;
|
||||||
|
AudioGeneratorMP3 *mp3;
|
||||||
|
|
||||||
|
// Called when a metadata event occurs (i.e. an ID3 tag, an ICY block, etc.
|
||||||
|
void MDCallback(void *cbData, const char *type, bool isUnicode, const char *string)
|
||||||
|
{
|
||||||
|
(void)cbData;
|
||||||
|
Serial.printf("ID3 callback for: %s = '", type);
|
||||||
|
|
||||||
|
if (isUnicode) {
|
||||||
|
string += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (*string) {
|
||||||
|
char a = *(string++);
|
||||||
|
if (isUnicode) {
|
||||||
|
string++;
|
||||||
|
}
|
||||||
|
Serial.printf("%c", a);
|
||||||
|
}
|
||||||
|
Serial.printf("'\n");
|
||||||
|
Serial.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void setup()
|
||||||
|
{
|
||||||
|
Serial.begin(115200);
|
||||||
|
delay(1000);
|
||||||
|
Serial.println();
|
||||||
|
audioLogger = &Serial;
|
||||||
|
SPIFFS.begin();
|
||||||
|
file = new AudioFileSourceSPIFFS();
|
||||||
|
id3 = NULL;
|
||||||
|
out = new AudioOutputSPDIF();
|
||||||
|
mp3 = new AudioGeneratorMP3();
|
||||||
|
String fileName = "";
|
||||||
|
|
||||||
|
// Find first MP3 file in SPIFF and play it
|
||||||
|
|
||||||
|
#ifdef ESP32
|
||||||
|
File dir, root = SPIFFS.open("/");
|
||||||
|
while ((dir = root.openNextFile())) {
|
||||||
|
if (String(dir.name()).endsWith(".mp3")) {
|
||||||
|
if (file->open(dir.name())) {
|
||||||
|
fileName = String(dir.name());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dir = root.openNextFile();
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
Dir dir = SPIFFS.openDir("");
|
||||||
|
while (dir.next()) {
|
||||||
|
if (dir.fileName().endsWith(".mp3")) {
|
||||||
|
if (file->open(dir.fileName().c_str())) {
|
||||||
|
fileName = dir.fileName();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (fileName.length() > 0) {
|
||||||
|
id3 = new AudioFileSourceID3(file);
|
||||||
|
id3->RegisterMetadataCB(MDCallback, (void*)"ID3TAG");
|
||||||
|
mp3->begin(id3, out);
|
||||||
|
Serial.printf("Playback of '%s' begins...\n", fileName.c_str());
|
||||||
|
} else {
|
||||||
|
Serial.println("Can't find .mp3 file in SPIFFS");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop()
|
||||||
|
{
|
||||||
|
if (mp3->isRunning()) {
|
||||||
|
if (!mp3->loop()) mp3->stop();
|
||||||
|
} else {
|
||||||
|
Serial.println("MP3 done");
|
||||||
|
delay(1000);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
#include <Arduino.h>
|
||||||
|
#ifdef ESP32
|
||||||
|
#include <WiFi.h>
|
||||||
|
#include "SPIFFS.h"
|
||||||
|
#else
|
||||||
|
#include <ESP8266WiFi.h>
|
||||||
|
#endif
|
||||||
|
#include "AudioFileSourceSPIFFS.h"
|
||||||
|
#include "AudioGeneratorOpus.h"
|
||||||
|
#include "AudioOutputI2S.h"
|
||||||
|
|
||||||
|
// The includes OPUS file is from Kevin MacLeod (incompetech.com), Licensed under Creative Commons: By Attribution 3.0, http://creativecommons.org/licenses/by/3.0/
|
||||||
|
|
||||||
|
AudioGeneratorOpus *opus;
|
||||||
|
AudioFileSourceSPIFFS *file;
|
||||||
|
AudioOutputI2S *out;
|
||||||
|
|
||||||
|
void setup()
|
||||||
|
{
|
||||||
|
WiFi.mode(WIFI_OFF);
|
||||||
|
Serial.begin(115200);
|
||||||
|
delay(1000);
|
||||||
|
SPIFFS.begin();
|
||||||
|
Serial.printf("Sample Opus playback begins...\n");
|
||||||
|
|
||||||
|
audioLogger = &Serial;
|
||||||
|
file = new AudioFileSourceSPIFFS("/gs-16b-2c-44100hz.opus");
|
||||||
|
out = new AudioOutputI2S();
|
||||||
|
opus = new AudioGeneratorOpus();
|
||||||
|
opus->begin(file, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop()
|
||||||
|
{
|
||||||
|
if (opus->isRunning()) {
|
||||||
|
if (!opus->loop()) opus->stop();
|
||||||
|
} else {
|
||||||
|
Serial.printf("Opus done\n");
|
||||||
|
delay(1000);
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
|
@ -0,0 +1,36 @@
|
||||||
|
#include "AudioFileSourcePROGMEM.h"
|
||||||
|
#include "AudioGeneratorRTTTL.h"
|
||||||
|
#include "AudioOutputI2S.h"
|
||||||
|
|
||||||
|
const char rudolph[] PROGMEM =
|
||||||
|
"Rudolph the Red Nosed Raindeer:d=8,o=5,b=250:g,4a,g,4e,4c6,4a,2g.,g,a,g,a,4g,4c6,2b.,4p,f,4g,f,4d,4b,4a,2g.,g,a,g,a,4g,4a,2e.,4p,g,4a,a,4e,4c6,4a,2g.,g,a,g,a,4g,4c6,2b.,4p,f,4g,f,4d,4b,4a,2g.,g,a,g,a,4g,4d6,2c.6,4p,4a,4a,4c6,4a,4g,4e,2g,4d,4e,4g,4a,4b,4b,2b,4c6,4c6,4b,4a,4g,4f,2d,g,4a,g,4e,4c6,4a,2g.,g,a,g,a,4g,4c6,2b.,4p,f,4g,f,4d,4b,4a,2g.,4g,4a,4g,4a,2g,2d6,1c.6.";
|
||||||
|
// Plenty more at: http://mines.lumpylumpy.com/Electronics/Computers/Software/Cpp/MFC/RingTones.RTTTL
|
||||||
|
|
||||||
|
AudioGeneratorRTTTL *rtttl;
|
||||||
|
AudioFileSourcePROGMEM *file;
|
||||||
|
AudioOutputI2S *out;
|
||||||
|
|
||||||
|
void setup()
|
||||||
|
{
|
||||||
|
Serial.begin(115200);
|
||||||
|
delay(1000);
|
||||||
|
|
||||||
|
Serial.printf("RTTTL start\n");
|
||||||
|
|
||||||
|
audioLogger = &Serial;
|
||||||
|
file = new AudioFileSourcePROGMEM( rudolph, strlen_P(rudolph) );
|
||||||
|
out = new AudioOutputI2S();
|
||||||
|
rtttl = new AudioGeneratorRTTTL();
|
||||||
|
rtttl->begin(file, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop()
|
||||||
|
{
|
||||||
|
if (rtttl->isRunning()) {
|
||||||
|
if (!rtttl->loop()) rtttl->stop();
|
||||||
|
} else {
|
||||||
|
Serial.printf("RTTTL done\n");
|
||||||
|
delay(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
#include <Arduino.h>
|
||||||
|
#ifdef ESP32
|
||||||
|
#include <WiFi.h>
|
||||||
|
#else
|
||||||
|
#include <ESP8266WiFi.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "AudioFileSourcePROGMEM.h"
|
||||||
|
#include "AudioGeneratorWAV.h"
|
||||||
|
#include "AudioOutputI2SNoDAC.h"
|
||||||
|
|
||||||
|
// VIOLA sample taken from https://ccrma.stanford.edu/~jos/pasp/Sound_Examples.html
|
||||||
|
#include "viola.h"
|
||||||
|
|
||||||
|
AudioGeneratorWAV *wav;
|
||||||
|
AudioFileSourcePROGMEM *file;
|
||||||
|
AudioOutputI2SNoDAC *out;
|
||||||
|
|
||||||
|
void setup()
|
||||||
|
{
|
||||||
|
WiFi.mode(WIFI_OFF);
|
||||||
|
Serial.begin(115200);
|
||||||
|
delay(1000);
|
||||||
|
Serial.printf("WAV start\n");
|
||||||
|
|
||||||
|
audioLogger = &Serial;
|
||||||
|
file = new AudioFileSourcePROGMEM( viola, sizeof(viola) );
|
||||||
|
out = new AudioOutputI2SNoDAC();
|
||||||
|
wav = new AudioGeneratorWAV();
|
||||||
|
wav->begin(file, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop()
|
||||||
|
{
|
||||||
|
if (wav->isRunning()) {
|
||||||
|
if (!wav->loop()) wav->stop();
|
||||||
|
} else {
|
||||||
|
Serial.printf("WAV done\n");
|
||||||
|
delay(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,107 @@
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
#ifdef ESP32
|
||||||
|
#include <WiFi.h>
|
||||||
|
#else
|
||||||
|
#include <ESP8266WiFi.h>
|
||||||
|
#endif
|
||||||
|
#include "AudioFileSourceICYStream.h"
|
||||||
|
#include "AudioFileSourceBuffer.h"
|
||||||
|
#include "AudioGeneratorMP3.h"
|
||||||
|
#include "AudioOutputI2SNoDAC.h"
|
||||||
|
|
||||||
|
// To run, set your ESP8266 build to 160MHz, update the SSID info, and upload.
|
||||||
|
|
||||||
|
// Enter your WiFi setup here:
|
||||||
|
#ifndef STASSID
|
||||||
|
#define STASSID "your-ssid"
|
||||||
|
#define STAPSK "your-password"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
const char* ssid = STASSID;
|
||||||
|
const char* password = STAPSK;
|
||||||
|
|
||||||
|
// Randomly picked URL
|
||||||
|
const char *URL="http://streaming.shoutcast.com/80sPlanet?lang=en-US";
|
||||||
|
|
||||||
|
AudioGeneratorMP3 *mp3;
|
||||||
|
AudioFileSourceICYStream *file;
|
||||||
|
AudioFileSourceBuffer *buff;
|
||||||
|
AudioOutputI2SNoDAC *out;
|
||||||
|
|
||||||
|
// Called when a metadata event occurs (i.e. an ID3 tag, an ICY block, etc.
|
||||||
|
void MDCallback(void *cbData, const char *type, bool isUnicode, const char *string)
|
||||||
|
{
|
||||||
|
const char *ptr = reinterpret_cast<const char *>(cbData);
|
||||||
|
(void) isUnicode; // Punt this ball for now
|
||||||
|
// Note that the type and string may be in PROGMEM, so copy them to RAM for printf
|
||||||
|
char s1[32], s2[64];
|
||||||
|
strncpy_P(s1, type, sizeof(s1));
|
||||||
|
s1[sizeof(s1)-1]=0;
|
||||||
|
strncpy_P(s2, string, sizeof(s2));
|
||||||
|
s2[sizeof(s2)-1]=0;
|
||||||
|
Serial.printf("METADATA(%s) '%s' = '%s'\n", ptr, s1, s2);
|
||||||
|
Serial.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called when there's a warning or error (like a buffer underflow or decode hiccup)
|
||||||
|
void StatusCallback(void *cbData, int code, const char *string)
|
||||||
|
{
|
||||||
|
const char *ptr = reinterpret_cast<const char *>(cbData);
|
||||||
|
// Note that the string may be in PROGMEM, so copy it to RAM for printf
|
||||||
|
char s1[64];
|
||||||
|
strncpy_P(s1, string, sizeof(s1));
|
||||||
|
s1[sizeof(s1)-1]=0;
|
||||||
|
Serial.printf("STATUS(%s) '%d' = '%s'\n", ptr, code, s1);
|
||||||
|
Serial.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void setup()
|
||||||
|
{
|
||||||
|
Serial.begin(115200);
|
||||||
|
delay(1000);
|
||||||
|
Serial.println("Connecting to WiFi");
|
||||||
|
|
||||||
|
WiFi.disconnect();
|
||||||
|
WiFi.softAPdisconnect(true);
|
||||||
|
WiFi.mode(WIFI_STA);
|
||||||
|
|
||||||
|
WiFi.begin(ssid, password);
|
||||||
|
|
||||||
|
// Try forever
|
||||||
|
while (WiFi.status() != WL_CONNECTED) {
|
||||||
|
Serial.println("...Connecting to WiFi");
|
||||||
|
delay(1000);
|
||||||
|
}
|
||||||
|
Serial.println("Connected");
|
||||||
|
|
||||||
|
audioLogger = &Serial;
|
||||||
|
file = new AudioFileSourceICYStream(URL);
|
||||||
|
file->RegisterMetadataCB(MDCallback, (void*)"ICY");
|
||||||
|
buff = new AudioFileSourceBuffer(file, 2048);
|
||||||
|
buff->RegisterStatusCB(StatusCallback, (void*)"buffer");
|
||||||
|
out = new AudioOutputI2SNoDAC();
|
||||||
|
mp3 = new AudioGeneratorMP3();
|
||||||
|
mp3->RegisterStatusCB(StatusCallback, (void*)"mp3");
|
||||||
|
mp3->begin(buff, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void loop()
|
||||||
|
{
|
||||||
|
static int lastms = 0;
|
||||||
|
|
||||||
|
if (mp3->isRunning()) {
|
||||||
|
if (millis()-lastms > 1000) {
|
||||||
|
lastms = millis();
|
||||||
|
Serial.printf("Running for %d ms...\n", lastms);
|
||||||
|
Serial.flush();
|
||||||
|
}
|
||||||
|
if (!mp3->loop()) mp3->stop();
|
||||||
|
} else {
|
||||||
|
Serial.printf("MP3 done\n");
|
||||||
|
delay(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
|
@ -0,0 +1,104 @@
|
||||||
|
#include <Arduino.h>
|
||||||
|
#ifdef ESP32
|
||||||
|
#include <WiFi.h>
|
||||||
|
#else
|
||||||
|
#include <ESP8266WiFi.h>
|
||||||
|
#endif
|
||||||
|
#include "AudioFileSourceICYStream.h"
|
||||||
|
#include "AudioFileSourceSPIRAMBuffer.h"
|
||||||
|
#include "AudioGeneratorMP3.h"
|
||||||
|
#include "AudioOutputI2SNoDAC.h"
|
||||||
|
|
||||||
|
// To run, set your ESP8266 build to 160MHz, update the SSID info, and upload.
|
||||||
|
|
||||||
|
// Enter your WiFi setup here:
|
||||||
|
#ifndef STASSID
|
||||||
|
#define STASSID "your-ssid"
|
||||||
|
#define STAPSK "your-password"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
const char* ssid = STASSID;
|
||||||
|
const char* password = STAPSK;
|
||||||
|
|
||||||
|
// Randomly picked URL
|
||||||
|
const char *URL="http://kvbstreams.dyndns.org:8000/wkvi-am";
|
||||||
|
|
||||||
|
AudioGeneratorMP3 *mp3;
|
||||||
|
AudioFileSourceICYStream *file;
|
||||||
|
AudioFileSourceSPIRAMBuffer *buff;
|
||||||
|
AudioOutputI2SNoDAC *out;
|
||||||
|
|
||||||
|
// Called when a metadata event occurs (i.e. an ID3 tag, an ICY block, etc.
|
||||||
|
void MDCallback(void *cbData, const char *type, bool isUnicode, const char *string)
|
||||||
|
{
|
||||||
|
const char *ptr = reinterpret_cast<const char *>(cbData);
|
||||||
|
(void) isUnicode; // Punt this ball for now
|
||||||
|
// Note that the type and string may be in PROGMEM, so copy them to RAM for printf
|
||||||
|
Serial.printf_P(PSTR("METADATA(%s) '%s' = '%s'\n"), ptr, type, string);
|
||||||
|
Serial.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Called when there's a warning or error (like a buffer underflow or decode hiccup)
|
||||||
|
void StatusCallback(void *cbData, int code, const char *string)
|
||||||
|
{
|
||||||
|
const char *ptr = reinterpret_cast<const char *>(cbData);
|
||||||
|
static uint32_t lastTime = 0;
|
||||||
|
static int lastCode = -99999;
|
||||||
|
uint32_t now = millis();
|
||||||
|
if ((lastCode != code) || (now - lastTime > 1000)) {
|
||||||
|
Serial.printf_P(PSTR("STATUS(%s) '%d' = '%s'\n"), ptr, code, string);
|
||||||
|
Serial.flush();
|
||||||
|
lastTime = now;
|
||||||
|
lastCode = code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup()
|
||||||
|
{
|
||||||
|
Serial.begin(115200);
|
||||||
|
delay(1000);
|
||||||
|
Serial.println("Connecting to WiFi");
|
||||||
|
|
||||||
|
WiFi.disconnect();
|
||||||
|
WiFi.softAPdisconnect(true);
|
||||||
|
WiFi.mode(WIFI_STA);
|
||||||
|
|
||||||
|
WiFi.begin(ssid, password);
|
||||||
|
|
||||||
|
// Try forever
|
||||||
|
while (WiFi.status() != WL_CONNECTED) {
|
||||||
|
Serial.println("...Connecting to WiFi");
|
||||||
|
delay(1000);
|
||||||
|
}
|
||||||
|
Serial.println("Connected");
|
||||||
|
|
||||||
|
audioLogger = &Serial;
|
||||||
|
file = new AudioFileSourceICYStream(URL);
|
||||||
|
file->RegisterMetadataCB(MDCallback, (void*)"ICY");
|
||||||
|
// Initialize 23LC1024 SPI RAM buffer with chip select ion GPIO4 and ram size of 128KByte
|
||||||
|
buff = new AudioFileSourceSPIRAMBuffer(file, 4, 128*1024);
|
||||||
|
buff->RegisterStatusCB(StatusCallback, (void*)"buffer");
|
||||||
|
out = new AudioOutputI2SNoDAC();
|
||||||
|
mp3 = new AudioGeneratorMP3();
|
||||||
|
mp3->RegisterStatusCB(StatusCallback, (void*)"mp3");
|
||||||
|
mp3->begin(buff, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop()
|
||||||
|
{
|
||||||
|
static int lastms = 0;
|
||||||
|
|
||||||
|
if (mp3->isRunning()) {
|
||||||
|
if (millis()-lastms > 1000) {
|
||||||
|
lastms = millis();
|
||||||
|
Serial.printf("Running for %d ms...\n", lastms);
|
||||||
|
Serial.flush();
|
||||||
|
}
|
||||||
|
if (!mp3->loop()) mp3->stop();
|
||||||
|
} else {
|
||||||
|
Serial.printf("MP3 done\n");
|
||||||
|
delay(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,185 @@
|
||||||
|
// Talking Clock example, with speech taken from
|
||||||
|
// https://github.com/going-digital/Talkie/blob/master/Talkie/examples/Vocab_US_Clock/Vocab_US_Clock.ino
|
||||||
|
// Released under GPL v2
|
||||||
|
|
||||||
|
#ifdef ESP32
|
||||||
|
#include <WiFi.h>
|
||||||
|
#else
|
||||||
|
#include <ESP8266WiFi.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <time.h>
|
||||||
|
#include "AudioFileSourcePROGMEM.h"
|
||||||
|
#include "AudioGeneratorTalkie.h"
|
||||||
|
#include "AudioOutputI2S.h"
|
||||||
|
|
||||||
|
#ifndef STASSID
|
||||||
|
#define STASSID "NOBABIES"
|
||||||
|
#define STAPSK "ElephantsAreGreat"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
const char *ssid = STASSID;
|
||||||
|
const char *pass = STAPSK;
|
||||||
|
|
||||||
|
long timezone = 2;
|
||||||
|
byte daysavetime = 1;
|
||||||
|
|
||||||
|
uint8_t spTHE[] PROGMEM = {0x08,0xE8,0x3E,0x55,0x01,0xC3,0x86,0x27,0xAF,0x72,0x0D,0x4D,0x97,0xD5,0xBC,0x64,0x3C,0xF2,0x5C,0x51,0xF1,0x93,0x36,0x8F,0x4F,0x59,0x2A,0x42,0x7A,0x32,0xC3,0x64,0xFF,0x3F};
|
||||||
|
uint8_t spTIME[] PROGMEM = {0x0E,0x28,0xAC,0x2D,0x01,0x5D,0xB6,0x0D,0x33,0xF3,0x54,0xB3,0x60,0xBA,0x8C,0x54,0x5C,0xCD,0x2D,0xD4,0x32,0x73,0x0F,0x8E,0x34,0x33,0xCB,0x4A,0x25,0xD4,0x25,0x83,0x2C,0x2B,0xD5,0x50,0x97,0x08,0x32,0xEC,0xD4,0xDC,0x4C,0x33,0xC8,0x70,0x73,0x0F,0x33,0xCD,0x20,0xC3,0xCB,0x43,0xDD,0x3C,0xCD,0x8C,0x20,0x77,0x89,0xF4,0x94,0xB2,0xE2,0xE2,0x35,0x22,0x5D,0xD6,0x4A,0x8A,0x96,0xCC,0x36,0x25,0x2D,0xC9,0x9A,0x7B,0xC2,0x18,0x87,0x24,0x4B,0x1C,0xC9,0x50,0x19,0x92,0x2C,0x71,0x34,0x4B,0x45,0x8A,0x8B,0xC4,0x96,0xB6,0x5A,0x29,0x2A,0x92,0x5A,0xCA,0x53,0x96,0x20,0x05,0x09,0xF5,0x92,0x5D,0xBC,0xE8,0x58,0x4A,0xDD,0xAE,0x73,0xBD,0x65,0x4B,0x8D,0x78,0xCA,0x2B,0x4E,0xD8,0xD9,0xED,0x22,0x20,0x06,0x75,0x00,0x00,0x80,0xFF,0x07};
|
||||||
|
uint8_t spIS[] PROGMEM = {0x21,0x18,0x96,0x38,0xB7,0x14,0x8D,0x60,0x3A,0xA6,0xE8,0x51,0xB4,0xDC,0x2E,0x48,0x7B,0x5A,0xF1,0x70,0x1B,0xA3,0xEC,0x09,0xC6,0xCB,0xEB,0x92,0x3D,0xA7,0x69,0x1F,0xAF,0x71,0x89,0x9C,0xA2,0xB3,0xFC,0xCA,0x35,0x72,0x9A,0xD1,0xF0,0xAB,0x12,0xB3,0x2B,0xC6,0xCD,0x4F,0xCC,0x32,0x26,0x19,0x07,0xDF,0x0B,0x8F,0xB8,0xA4,0xED,0x7C,0xCF,0x23,0x62,0x8B,0x8E,0xF1,0x23,0x0A,0x8B,0x6E,0xCB,0xCE,0xEF,0x54,0x44,0x3C,0xDC,0x08,0x60,0x0B,0x37,0x01,0x1C,0x53,0x26,0x80,0x15,0x4E,0x14,0xB0,0x54,0x2B,0x02,0xA4,0x69,0xFF,0x7F};
|
||||||
|
uint8_t spA_M_[] PROGMEM = {0xCD,0xEF,0x86,0xAB,0x57,0x6D,0x0F,0xAF,0x71,0xAD,0x49,0x55,0x3C,0xFC,0x2E,0xC5,0xB7,0x5C,0xF1,0xF2,0x87,0x66,0xDD,0x4E,0xC5,0xC3,0xEF,0x92,0xE2,0x3A,0x65,0xB7,0xA0,0x09,0xAA,0x1B,0x97,0x54,0x82,0x2E,0x28,0x77,0x5C,0x52,0x09,0x1A,0xA3,0xB8,0x76,0x49,0x25,0x68,0x8C,0x73,0xDB,0x24,0x95,0xA0,0x32,0xA9,0x6B,0xA7,0xD9,0x82,0x26,0xA9,0x76,0x42,0xD6,0x08,0xBA,0xE1,0xE8,0x0E,0x5A,0x2B,0xEA,0x9E,0x3D,0x27,0x18,0xAD,0xA8,0x07,0xF1,0x98,0x90,0x35,0xA2,0x96,0x44,0xA3,0x5D,0x66,0x8B,0x6B,0x12,0xCD,0x32,0x85,0x25,0xC9,0x81,0x2D,0xC3,0x64,0x85,0x34,0x58,0x89,0x94,0x52,0x1C,0x52,0x2F,0x35,0xDA,0xC7,0x51,0x48,0x23,0x97,0xCC,0x2C,0x97,0x2E,0xF3,0x5C,0xF3,0xA2,0x14,0xBA,0x2C,0x48,0xCE,0xCA,0x76,0xE8,0x32,0x2F,0x34,0xB2,0xDB,0x85,0xC9,0x83,0x90,0xA8,0x2C,0x57,0x26,0x8F,0x9C,0xBD,0xA2,0x53,0xD9,0xC2,0x54,0x59,0x28,0x99,0x4B,0x2C,0x5D,0xFF,0x3F};
|
||||||
|
uint8_t spP_M_[] PROGMEM = {0x0E,0x98,0x41,0x54,0x00,0x43,0xA0,0x05,0xAB,0x42,0x8E,0x1D,0xA3,0x15,0xEC,0x4E,0x58,0xF7,0x92,0x66,0x70,0x1B,0x66,0xDB,0x73,0x99,0xC1,0xEB,0x98,0xED,0xD6,0x25,0x25,0x6F,0x70,0x92,0xDD,0x64,0xD8,0xFC,0x61,0xD0,0x66,0x83,0xD6,0x0A,0x86,0x23,0xAB,0x69,0xDA,0x2B,0x18,0x9E,0x3D,0x37,0x69,0x9D,0xA8,0x07,0x71,0x9F,0xA0,0xBD,0xA2,0x16,0xD5,0x7C,0x54,0xF6,0x88,0x6B,0x54,0x8B,0x34,0x49,0x2D,0x29,0x49,0x3C,0x34,0x64,0xA5,0x24,0x1B,0x36,0xD7,0x72,0x13,0x92,0xA4,0xC4,0x2D,0xC3,0xB3,0x4B,0xA3,0x62,0x0F,0x2B,0x37,0x6E,0x8B,0x5A,0xD4,0x3D,0xDD,0x9A,0x2D,0x50,0x93,0xF6,0x4C,0xAA,0xB6,0xC4,0x85,0x3B,0xB2,0xB1,0xD8,0x93,0x20,0x4D,0x8F,0x24,0xFF,0x0F};
|
||||||
|
uint8_t spOH[] PROGMEM = {0xC6,0xC9,0x71,0x5A,0xA2,0x92,0x14,0x2F,0x6E,0x97,0x9C,0x46,0x9D,0xDC,0xB0,0x4D,0x62,0x1B,0x55,0x70,0xDD,0x55,0xBE,0x0E,0x36,0xC1,0x33,0x37,0xA9,0xA7,0x51,0x1B,0xCF,0x3C,0xA5,0x9E,0x44,0xAC,0x3C,0x7D,0x98,0x7B,0x52,0x96,0x72,0x65,0x4B,0xF6,0x1A,0xD9,0xCA,0xF5,0x91,0x2D,0xA2,0x2A,0x4B,0xF7,0xFF,0x01};
|
||||||
|
uint8_t spOCLOCK[] PROGMEM = {0x21,0x4E,0x3D,0xB8,0x2B,0x19,0xBB,0x24,0x0E,0xE5,0xEC,0x60,0xE4,0xF2,0x90,0x13,0xD4,0x2A,0x11,0x80,0x00,0x42,0x69,0x26,0x40,0xD0,0x2B,0x04,0x68,0xE0,0x4D,0x00,0x3A,0x35,0x35,0x33,0xB6,0x51,0xD9,0x64,0x34,0x82,0xB4,0x9A,0x63,0x92,0x55,0x89,0x52,0x5B,0xCA,0x2E,0x34,0x25,0x4E,0x63,0x28,0x3A,0x50,0x95,0x26,0x8D,0xE6,0xAA,0x64,0x58,0xEA,0x92,0xCE,0xC2,0x46,0x15,0x9B,0x86,0xCD,0x2A,0x2E,0x37,0x00,0x00,0x00,0x0C,0xC8,0xDD,0x05,0x01,0xB9,0x33,0x21,0xA0,0x74,0xD7,0xFF,0x07};
|
||||||
|
uint8_t spONE[] PROGMEM = {0xCC,0x67,0x75,0x42,0x59,0x5D,0x3A,0x4F,0x9D,0x36,0x63,0xB7,0x59,0xDC,0x30,0x5B,0x5C,0x23,0x61,0xF3,0xE2,0x1C,0xF1,0xF0,0x98,0xC3,0x4B,0x7D,0x39,0xCA,0x1D,0x2C,0x2F,0xB7,0x15,0xEF,0x70,0x79,0xBC,0xD2,0x46,0x7C,0x52,0xE5,0xF1,0x4A,0x6A,0xB3,0x71,0x47,0xC3,0x2D,0x39,0x34,0x4B,0x23,0x35,0xB7,0x7A,0x55,0x33,0x8F,0x59,0xDC,0xA2,0x44,0xB5,0xBC,0x66,0x72,0x8B,0x64,0xF5,0xF6,0x98,0xC1,0x4D,0x42,0xD4,0x27,0x62,0x38,0x2F,0x4A,0xB6,0x9C,0x88,0x68,0xBC,0xA6,0x95,0xF8,0x5C,0xA1,0x09,0x86,0x77,0x91,0x11,0x5B,0xFF,0x0F};
|
||||||
|
uint8_t spTWO[] PROGMEM = {0x0E,0x38,0x6E,0x25,0x00,0xA3,0x0D,0x3A,0xA0,0x37,0xC5,0xA0,0x05,0x9E,0x56,0x35,0x86,0xAA,0x5E,0x8C,0xA4,0x82,0xB2,0xD7,0x74,0x31,0x22,0x69,0xAD,0x1C,0xD3,0xC1,0xD0,0xFA,0x28,0x2B,0x2D,0x47,0xC3,0x1B,0xC2,0xC4,0xAE,0xC6,0xCD,0x9C,0x48,0x53,0x9A,0xFF,0x0F};
|
||||||
|
uint8_t spTHREE[] PROGMEM = {0x02,0xD8,0x2E,0x9C,0x01,0xDB,0xA6,0x33,0x60,0xFB,0x30,0x01,0xEC,0x20,0x12,0x8C,0xE4,0xD8,0xCA,0x32,0x96,0x73,0x63,0x41,0x39,0x89,0x98,0xC1,0x4D,0x0D,0xED,0xB0,0x2A,0x05,0x37,0x0F,0xB4,0xA5,0xAE,0x5C,0xDC,0x36,0xD0,0x83,0x2F,0x4A,0x71,0x7B,0x03,0xF7,0x38,0x59,0xCD,0xED,0x1E,0xB4,0x6B,0x14,0x35,0xB7,0x6B,0x94,0x99,0x91,0xD5,0xDC,0x26,0x48,0x77,0x4B,0x66,0x71,0x1B,0x21,0xDB,0x2D,0x8A,0xC9,0x6D,0x88,0xFC,0x26,0x28,0x3A,0xB7,0x21,0xF4,0x1F,0xA3,0x65,0xBC,0x02,0x38,0xBB,0x3D,0x8E,0xF0,0x2B,0xE2,0x08,0xB7,0x34,0xFF,0x0F};
|
||||||
|
uint8_t spFOUR[] PROGMEM = {0x0C,0x18,0xB6,0x9A,0x01,0xC3,0x75,0x09,0x60,0xD8,0x0E,0x09,0x30,0xA0,0x9B,0xB6,0xA0,0xBB,0xB0,0xAA,0x16,0x4E,0x82,0xEB,0xEA,0xA9,0xFA,0x59,0x49,0x9E,0x59,0x23,0x9A,0x27,0x3B,0x78,0x66,0xAE,0x4A,0x9C,0x9C,0xE0,0x99,0xD3,0x2A,0xBD,0x72,0x92,0xEF,0xE6,0x88,0xE4,0x45,0x4D,0x7E,0x98,0x2D,0x62,0x67,0x37,0xF9,0xA1,0x37,0xA7,0x6C,0x94,0xE4,0xC7,0x1E,0xDC,0x3C,0xA5,0x83,0x1F,0x8B,0xEB,0x52,0x0E,0x0E,0x7E,0x2E,0x4E,0xC7,0x31,0xD2,0x79,0xA5,0x3A,0x0D,0xD9,0xC4,0xFF,0x07};
|
||||||
|
uint8_t spFIVE[] PROGMEM = {0x02,0xE8,0x3E,0x8C,0x01,0xDD,0x65,0x08,0x60,0x98,0x4C,0x06,0x34,0x93,0xCE,0x80,0xE6,0xDA,0x9A,0x14,0x6B,0xAA,0x47,0xD1,0x5E,0x56,0xAA,0x6D,0x56,0xCD,0x78,0xD9,0xA9,0x1C,0x67,0x05,0x83,0xE1,0xA4,0xBA,0x38,0xEE,0x16,0x86,0x9B,0xFA,0x60,0x87,0x5B,0x18,0x6E,0xEE,0x8B,0x1D,0x6E,0x61,0xB9,0x69,0x36,0x65,0xBA,0x8D,0xE5,0xE5,0x3E,0x1C,0xE9,0x0E,0x96,0x9B,0x5B,0xAB,0x95,0x2B,0x58,0x6E,0xCE,0xE5,0x3A,0x6A,0xF3,0xB8,0x35,0x84,0x7B,0x05,0xA3,0xE3,0x36,0xEF,0x92,0x19,0xB4,0x86,0xDB,0xB4,0x69,0xB4,0xD1,0x2A,0x4E,0x65,0x9A,0x99,0xCE,0x28,0xD9,0x85,0x71,0x4C,0x18,0x6D,0x67,0x47,0xC6,0x5E,0x53,0x4A,0x9C,0xB5,0xE2,0x85,0x45,0x26,0xFE,0x7F};
|
||||||
|
uint8_t spSIX[] PROGMEM = {0x0E,0xD8,0xAE,0xDD,0x03,0x0E,0x38,0xA6,0xD2,0x01,0xD3,0xB4,0x2C,0xAD,0x6A,0x35,0x9D,0xB1,0x7D,0xDC,0xEE,0xC4,0x65,0xD7,0xF1,0x72,0x47,0x24,0xB3,0x19,0xD9,0xD9,0x05,0x70,0x40,0x49,0xEA,0x02,0x98,0xBE,0x42,0x01,0xDF,0xA4,0x69,0x40,0x00,0xDF,0x95,0xFC,0x3F};
|
||||||
|
uint8_t spSEVEN[] PROGMEM = {0x02,0xB8,0x3A,0x8C,0x01,0xDF,0xA4,0x73,0x40,0x01,0x47,0xB9,0x2F,0x33,0x3B,0x73,0x5F,0x53,0x7C,0xEC,0x9A,0xC5,0x63,0xD5,0xD1,0x75,0xAE,0x5B,0xFC,0x64,0x5C,0x35,0x87,0x91,0xF1,0x83,0x36,0xB5,0x68,0x55,0xC5,0x6F,0xDA,0x45,0x2D,0x1C,0x2D,0xB7,0x38,0x37,0x9F,0x60,0x3C,0xBC,0x9A,0x85,0xA3,0x25,0x66,0xF7,0x8A,0x57,0x1C,0xA9,0x67,0x56,0xCA,0x5E,0xF0,0xB2,0x16,0xB2,0xF1,0x89,0xCE,0x8B,0x92,0x25,0xC7,0x2B,0x33,0xCF,0x48,0xB1,0x99,0xB4,0xF3,0xFF};
|
||||||
|
uint8_t spEIGHT[] PROGMEM = {0xC3,0x6C,0x86,0xB3,0x27,0x6D,0x0F,0xA7,0x48,0x99,0x4E,0x55,0x3C,0xBC,0x22,0x65,0x36,0x4D,0xD1,0xF0,0x32,0xD3,0xBE,0x34,0xDA,0xC3,0xEB,0x82,0xE2,0xDA,0x65,0x35,0xAF,0x31,0xF2,0x6B,0x97,0x95,0xBC,0x86,0xD8,0x6F,0x82,0xA6,0x73,0x0B,0xC6,0x9E,0x72,0x99,0xCC,0xCB,0x02,0xAD,0x3C,0x9A,0x10,0x60,0xAB,0x62,0x05,0x2C,0x37,0x84,0x00,0xA9,0x73,0x00,0x00,0xFE,0x1F};
|
||||||
|
uint8_t spNINE[] PROGMEM = {0xCC,0xA1,0x26,0xBB,0x83,0x93,0x18,0xCF,0x4A,0xAD,0x2E,0x31,0xED,0x3C,0xA7,0x24,0x26,0xC3,0x54,0xF1,0x92,0x64,0x8B,0x8A,0x98,0xCB,0x2B,0x2E,0x34,0x53,0x2D,0x0E,0x2F,0x57,0xB3,0x0C,0x0D,0x3C,0xBC,0x3C,0x4C,0x4B,0xCA,0xF4,0xF0,0x72,0x0F,0x6E,0x49,0x53,0xCD,0xCB,0x53,0x2D,0x35,0x4D,0x0F,0x2F,0x0F,0xD7,0x0C,0x0D,0x3D,0xBC,0xDC,0x4D,0xD3,0xDD,0xC2,0xF0,0x72,0x52,0x4F,0x57,0x9B,0xC3,0xAB,0x89,0xBD,0x42,0x2D,0x0F,0xAF,0x5A,0xD1,0x71,0x91,0x55,0xBC,0x2C,0xC5,0x3B,0xD8,0x65,0xF2,0x82,0x94,0x18,0x4E,0x3B,0xC1,0x73,0x42,0x32,0x33,0x15,0x45,0x4F,0x79,0x52,0x6A,0x55,0xA6,0xA3,0xFF,0x07};
|
||||||
|
uint8_t spTEN[] PROGMEM = {0x0E,0xD8,0xB1,0xDD,0x01,0x3D,0xA8,0x24,0x7B,0x04,0x27,0x76,0x77,0xDC,0xEC,0xC2,0xC5,0x23,0x84,0xCD,0x72,0x9A,0x51,0xF7,0x62,0x45,0xC7,0xEB,0x4E,0x35,0x4A,0x14,0x2D,0xBF,0x45,0xB6,0x0A,0x75,0xB8,0xFC,0x16,0xD9,0x2A,0xD9,0xD6,0x0A,0x5A,0x10,0xCD,0xA2,0x48,0x23,0xA8,0x81,0x35,0x4B,0x2C,0xA7,0x20,0x69,0x0A,0xAF,0xB6,0x15,0x82,0xA4,0x29,0x3C,0xC7,0x52,0x08,0xA2,0x22,0xCF,0x68,0x4B,0x2E,0xF0,0x8A,0xBD,0xA3,0x2C,0xAB,0x40,0x1B,0xCE,0xAA,0xB2,0x6C,0x82,0x40,0x4D,0x7D,0xC2,0x89,0x88,0x8A,0x61,0xCC,0x74,0xD5,0xFF,0x0F};
|
||||||
|
uint8_t spELEVEN[] PROGMEM = {0xC3,0xCD,0x76,0x5C,0xAE,0x14,0x0F,0x37,0x9B,0x71,0xDE,0x92,0x55,0xBC,0x2C,0x27,0x70,0xD3,0x76,0xF0,0x83,0x5E,0xA3,0x5E,0x5A,0xC1,0xF7,0x61,0x58,0xA7,0x19,0x35,0x3F,0x99,0x31,0xDE,0x52,0x74,0xFC,0xA2,0x26,0x64,0x4B,0xD1,0xF1,0xAB,0xAE,0xD0,0x2D,0xC5,0xC7,0x2F,0x36,0xDD,0x27,0x15,0x0F,0x3F,0xD9,0x08,0x9F,0x62,0xE4,0xC2,0x2C,0xD4,0xD8,0xD3,0x89,0x0B,0x1B,0x57,0x11,0x0B,0x3B,0xC5,0xCF,0xD6,0xCC,0xC6,0x64,0x35,0xAF,0x18,0x73,0x1F,0xA1,0x5D,0xBC,0x62,0x45,0xB3,0x45,0x51,0xF0,0xA2,0x62,0xAB,0x4A,0x5B,0xC9,0x4B,0x8A,0x2D,0xB3,0x6C,0x06,0x2F,0x29,0xB2,0xAC,0x8A,0x18,0xBC,0x28,0xD9,0xAA,0xD2,0x92,0xF1,0xBC,0xE0,0x98,0x8C,0x48,0xCC,0x17,0x52,0xA3,0x27,0x6D,0x93,0xD0,0x4B,0x8E,0x0E,0x77,0x02,0x00,0xFF,0x0F};
|
||||||
|
uint8_t spTWELVE[] PROGMEM = {0x06,0x28,0x46,0xD3,0x01,0x25,0x06,0x13,0x20,0xBA,0x70,0x70,0xB6,0x79,0xCA,0x36,0xAE,0x28,0x38,0xE1,0x29,0xC5,0x35,0xA3,0xE6,0xC4,0x16,0x6A,0x53,0x8C,0x97,0x9B,0x72,0x86,0x4F,0x28,0x1A,0x6E,0x0A,0x59,0x36,0xAE,0x68,0xF8,0x29,0x67,0xFA,0x06,0xA3,0x16,0xC4,0x96,0xE6,0x53,0xAC,0x5A,0x9C,0x56,0x72,0x77,0x31,0x4E,0x49,0x5C,0x8D,0x5B,0x29,0x3B,0x24,0x61,0x1E,0x6C,0x9B,0x6C,0x97,0xF8,0xA7,0x34,0x19,0x92,0x4C,0x62,0x9E,0x72,0x65,0x58,0x12,0xB1,0x7E,0x09,0xD5,0x2E,0x53,0xC5,0xBA,0x36,0x6B,0xB9,0x2D,0x17,0x05,0xEE,0x9A,0x6E,0x8E,0x05,0x50,0x6C,0x19,0x07,0x18,0x50,0xBD,0x3B,0x01,0x92,0x08,0x41,0x40,0x10,0xA6,0xFF,0x0F};
|
||||||
|
uint8_t spTHIRTEEN[] PROGMEM = {0x08,0xE8,0x2C,0x15,0x01,0x43,0x07,0x13,0xE0,0x98,0xB4,0xA6,0x35,0xA9,0x1E,0xDE,0x56,0x8E,0x53,0x9C,0x7A,0xE7,0xCA,0x5E,0x76,0x8D,0x94,0xE5,0x2B,0xAB,0xD9,0xB5,0x62,0xA4,0x9C,0xE4,0xE6,0xB4,0x41,0x1E,0x7C,0xB6,0x93,0xD7,0x16,0x99,0x5A,0xCD,0x61,0x76,0x55,0xC2,0x91,0x61,0x1B,0xC0,0x01,0x5D,0x85,0x05,0xE0,0x68,0x51,0x07,0x1C,0xA9,0x64,0x80,0x1D,0x4C,0x9C,0x95,0x88,0xD4,0x04,0x3B,0x4D,0x4E,0x21,0x5C,0x93,0xA8,0x26,0xB9,0x05,0x4B,0x6E,0xA0,0xE2,0xE4,0x57,0xC2,0xB9,0xC1,0xB2,0x93,0x5F,0x09,0xD7,0x24,0xCB,0x4E,0x41,0x25,0x54,0x1D,0x62,0x3B,0x05,0x8D,0x52,0x57,0xAA,0xAD,0x10,0x24,0x26,0xE3,0xE1,0x36,0x5D,0x10,0x85,0xB4,0x97,0x85,0x72,0x41,0x14,0x52,0x5E,0x1A,0xCA,0xF9,0x91,0x6B,0x7A,0x5B,0xC4,0xE0,0x17,0x2D,0x54,0x1D,0x92,0x8C,0x1F,0x25,0x4B,0x8F,0xB2,0x16,0x41,0xA1,0x4A,0x3E,0xE6,0xFA,0xFF,0x01};
|
||||||
|
uint8_t spFOURTEEN[] PROGMEM = {0x0C,0x58,0xAE,0x5C,0x01,0xD9,0x87,0x07,0x51,0xB7,0x25,0xB3,0x8A,0x15,0x2C,0xF7,0x1C,0x35,0x87,0x4D,0xB2,0xDD,0x53,0xCE,0x28,0x2B,0xC9,0x0E,0x97,0x2D,0xBD,0x2A,0x17,0x27,0x76,0x8E,0xD2,0x9A,0x6C,0x80,0x94,0x71,0x00,0x00,0x02,0xB0,0x58,0x58,0x00,0x9E,0x0B,0x0A,0xC0,0xB2,0xCE,0xC1,0xC8,0x98,0x7A,0x52,0x95,0x24,0x2B,0x11,0xED,0x36,0xD4,0x92,0xDC,0x4C,0xB5,0xC7,0xC8,0x53,0xF1,0x2A,0xE5,0x1A,0x17,0x55,0xC5,0xAF,0x94,0xBB,0xCD,0x1C,0x26,0xBF,0x52,0x9A,0x72,0x53,0x98,0xFC,0xC2,0x68,0xD2,0x4D,0x61,0xF0,0xA3,0x90,0xB6,0xD6,0x50,0xC1,0x8F,0x42,0xDA,0x4A,0x43,0x39,0x3F,0x48,0x2D,0x6B,0x33,0xF9,0xFF};
|
||||||
|
uint8_t spFIFTEEN[] PROGMEM = {0x08,0xE8,0x2A,0x0D,0x01,0xDD,0xBA,0x31,0x60,0x6A,0xF7,0xA0,0xAE,0x54,0xAA,0x5A,0x76,0x97,0xD9,0x34,0x69,0xEF,0x32,0x1E,0x66,0xE1,0xE2,0xB3,0x43,0xA9,0x18,0x55,0x92,0x4E,0x37,0x2D,0x67,0x6F,0xDF,0xA2,0x5A,0xB6,0x04,0x30,0x55,0xA8,0x00,0x86,0x09,0xE7,0x00,0x01,0x16,0x17,0x05,0x70,0x40,0x57,0xE5,0x01,0xF8,0x21,0x34,0x00,0xD3,0x19,0x33,0x80,0x89,0x9A,0x62,0x34,0x4C,0xD5,0x49,0xAE,0x8B,0x53,0x09,0xF7,0x26,0xD9,0x6A,0x7E,0x23,0x5C,0x13,0x12,0xB3,0x04,0x9D,0x50,0x4F,0xB1,0xAD,0x14,0x15,0xC2,0xD3,0xA1,0xB6,0x42,0x94,0xA8,0x8C,0x87,0xDB,0x74,0xB1,0x70,0x59,0xE1,0x2E,0xC9,0xC5,0x81,0x5B,0x55,0xA4,0x4C,0x17,0x47,0xC1,0x6D,0xE3,0x81,0x53,0x9C,0x84,0x6A,0x46,0xD9,0x4C,0x51,0x31,0x42,0xD9,0x66,0xC9,0x44,0x85,0x29,0x6A,0x9B,0xAD,0xFF,0x07};
|
||||||
|
uint8_t spSIXTEEN[] PROGMEM = {0x0A,0x58,0x5A,0x5D,0x00,0x93,0x97,0x0B,0x60,0xA9,0x48,0x05,0x0C,0x15,0xAE,0x80,0xAD,0x3D,0x14,0x30,0x7D,0xD9,0x50,0x92,0x92,0xAC,0x0D,0xC5,0xCD,0x2A,0x82,0xAA,0x3B,0x98,0x04,0xB3,0x4A,0xC8,0x9A,0x90,0x05,0x09,0x68,0x51,0xD4,0x01,0x23,0x9F,0x1A,0x60,0xA9,0x12,0x03,0xDC,0x50,0x81,0x80,0x22,0xDC,0x20,0x00,0xCB,0x06,0x3A,0x60,0x16,0xE3,0x64,0x64,0x42,0xDD,0xCD,0x6A,0x8A,0x5D,0x28,0x75,0x07,0xA9,0x2A,0x5E,0x65,0x34,0xED,0x64,0xBB,0xF8,0x85,0xF2,0x94,0x8B,0xAD,0xE4,0x37,0x4A,0x5B,0x21,0xB6,0x52,0x50,0x19,0xAD,0xA7,0xD8,0x4A,0x41,0x14,0xDA,0x5E,0x12,0x3A,0x04,0x91,0x4B,0x7B,0x69,0xA8,0x10,0x24,0x2E,0xE5,0xA3,0x81,0x52,0x90,0x94,0x5A,0x55,0x98,0x32,0x41,0x50,0xCC,0x93,0x2E,0x47,0x85,0x89,0x1B,0x5B,0x5A,0x62,0x04,0x44,0xE3,0x02,0x80,0x80,0x64,0xDD,0xFF,0x1F};
|
||||||
|
uint8_t spSEVENTEEN[] PROGMEM = {0x02,0x98,0x3A,0x42,0x00,0x5B,0xA6,0x09,0x60,0xDB,0x52,0x06,0x1C,0x93,0x29,0x80,0xA9,0x52,0x87,0x9A,0xB5,0x99,0x4F,0xC8,0x3E,0x46,0xD6,0x5E,0x7E,0x66,0xFB,0x98,0xC5,0x5A,0xC6,0x9A,0x9C,0x63,0x15,0x6B,0x11,0x13,0x8A,0x9C,0x97,0xB9,0x9A,0x5A,0x39,0x71,0xEE,0xD2,0x29,0xC2,0xA6,0xB8,0x58,0x59,0x99,0x56,0x14,0xA3,0xE1,0x26,0x19,0x19,0xE3,0x8C,0x93,0x17,0xB4,0x46,0xB5,0x88,0x71,0x9E,0x97,0x9E,0xB1,0x2C,0xC5,0xF8,0x56,0xC4,0x58,0xA3,0x1C,0xE1,0x33,0x9D,0x13,0x41,0x8A,0x43,0x58,0xAD,0x95,0xA9,0xDB,0x36,0xC0,0xD1,0xC9,0x0E,0x58,0x4E,0x45,0x01,0x23,0xA9,0x04,0x37,0x13,0xAE,0x4D,0x65,0x52,0x82,0xCA,0xA9,0x37,0x99,0x4D,0x89,0xBA,0xC0,0xBC,0x14,0x36,0x25,0xEA,0x1C,0x73,0x52,0x1D,0x97,0xB8,0x33,0xAC,0x0E,0x75,0x9C,0xE2,0xCE,0xB0,0xDA,0xC3,0x51,0x4A,0x1A,0xA5,0xCA,0x70,0x5B,0x21,0xCE,0x4C,0x26,0xD2,0x6C,0xBA,0x38,0x71,0x2E,0x1F,0x2D,0xED,0xE2,0x24,0xB8,0xBC,0x3D,0x52,0x88,0xAB,0x50,0x8E,0xA8,0x48,0x22,0x4E,0x42,0xA0,0x26,0x55,0xFD,0x3F};
|
||||||
|
uint8_t spEIGHTEEN[] PROGMEM = {0x2E,0x9C,0xD1,0x4D,0x54,0xEC,0x2C,0xBF,0x1B,0x8A,0x99,0x70,0x7C,0xFC,0x2E,0x29,0x6F,0x52,0xF6,0xF1,0xBA,0x20,0xBF,0x36,0xD9,0xCD,0xED,0x0C,0xF3,0x27,0x64,0x17,0x73,0x2B,0xA2,0x99,0x90,0x65,0xEC,0xED,0x40,0x73,0x32,0x12,0xB1,0xAF,0x30,0x35,0x0B,0xC7,0x00,0xE0,0x80,0xAE,0xDD,0x1C,0x70,0x43,0xAA,0x03,0x86,0x51,0x36,0xC0,0x30,0x64,0xCE,0x4C,0x98,0xFB,0x5C,0x65,0x07,0xAF,0x10,0xEA,0x0B,0x66,0x1B,0xFC,0x46,0xA8,0x3E,0x09,0x4D,0x08,0x2A,0xA6,0x3E,0x67,0x36,0x21,0x2A,0x98,0x67,0x9D,0x15,0xA7,0xA8,0x60,0xEE,0xB6,0x94,0x99,0xA2,0x4A,0x78,0x22,0xC2,0xA6,0x8B,0x8C,0x8E,0xCC,0x4C,0x8A,0x2E,0x8A,0x4C,0xD3,0x57,0x03,0x87,0x28,0x71,0x09,0x1F,0x2B,0xE4,0xA2,0xC4,0xC5,0x6D,0xAD,0x54,0x88,0xB2,0x63,0xC9,0xF2,0x50,0x2E,0x8A,0x4A,0x38,0x4A,0xEC,0x88,0x28,0x08,0xE3,0x28,0x49,0xF3,0xFF};
|
||||||
|
uint8_t spNINETEEN[] PROGMEM = {0xC2,0xEA,0x8A,0x95,0x2B,0x6A,0x05,0x3F,0x71,0x71,0x5F,0x0D,0x12,0xFC,0x28,0x25,0x62,0x35,0xF0,0xF0,0xB3,0x48,0x1E,0x0F,0xC9,0xCB,0x2F,0x45,0x7C,0x2C,0x25,0x1F,0xBF,0x14,0xB3,0x2C,0xB5,0x75,0xFC,0x5A,0x5C,0xA3,0x5D,0xE1,0xF1,0x7A,0x76,0xB3,0x4E,0x45,0xC7,0xED,0x96,0x23,0x3B,0x18,0x37,0x7B,0x18,0xCC,0x09,0x51,0x13,0x4C,0xAB,0x6C,0x4C,0x4B,0x96,0xD2,0x49,0xAA,0x36,0x0B,0xC5,0xC2,0x20,0x26,0x27,0x35,0x63,0x09,0x3D,0x30,0x8B,0xF0,0x48,0x5C,0xCA,0x61,0xDD,0xCB,0xCD,0x91,0x03,0x8E,0x4B,0x76,0xC0,0xCC,0x4D,0x06,0x98,0x31,0x31,0x98,0x99,0x70,0x6D,0x2A,0xA3,0xE4,0x16,0xCA,0xBD,0xCE,0x5C,0x92,0x57,0x28,0xCF,0x09,0x69,0x2E,0x7E,0xA5,0x3C,0x63,0xA2,0x30,0x05,0x95,0xD2,0x74,0x98,0xCD,0x14,0x54,0xCA,0x53,0xA9,0x96,0x52,0x50,0x28,0x6F,0xBA,0xCB,0x0C,0x41,0x50,0xDE,0x65,0x2E,0xD3,0x05,0x89,0x4B,0x7B,0x6B,0x20,0x17,0x44,0xAE,0xED,0x23,0x81,0x52,0x90,0x85,0x73,0x57,0xD0,0x72,0x41,0xB1,0x02,0xDE,0x2E,0xDB,0x04,0x89,0x05,0x79,0xBB,0x62,0xE5,0x76,0x11,0xCA,0x61,0x0E,0xFF,0x1F};
|
||||||
|
uint8_t spTWENTY[] PROGMEM = {0x01,0x98,0xD1,0xC2,0x00,0xCD,0xA4,0x32,0x20,0x79,0x13,0x04,0x28,0xE7,0x92,0xDC,0x70,0xCC,0x5D,0xDB,0x76,0xF3,0xD2,0x32,0x0B,0x0B,0x5B,0xC3,0x2B,0xCD,0xD4,0xDD,0x23,0x35,0xAF,0x44,0xE1,0xF0,0xB0,0x6D,0x3C,0xA9,0xAD,0x3D,0x35,0x0E,0xF1,0x0C,0x8B,0x28,0xF7,0x34,0x01,0x68,0x22,0xCD,0x00,0xC7,0xA4,0x04,0xBB,0x32,0xD6,0xAC,0x56,0x9C,0xDC,0xCA,0x28,0x66,0x53,0x51,0x70,0x2B,0xA5,0xBC,0x0D,0x9A,0xC1,0xEB,0x14,0x73,0x37,0x29,0x19,0xAF,0x33,0x8C,0x3B,0xA7,0x24,0xBC,0x42,0xB0,0xB7,0x59,0x09,0x09,0x3C,0x96,0xE9,0xF4,0x58,0xFF,0x0F};
|
||||||
|
uint8_t spTHIRTY[] PROGMEM = {0x08,0x98,0xD6,0x15,0x01,0x43,0xBB,0x0A,0x20,0x1B,0x8B,0xE5,0x16,0xA3,0x1E,0xB6,0xB6,0x96,0x97,0x3C,0x57,0xD4,0x2A,0x5E,0x7E,0x4E,0xD8,0xE1,0x6B,0x7B,0xF8,0x39,0x63,0x0D,0x9F,0x95,0xE1,0xE7,0x4C,0x76,0xBC,0x91,0x5B,0x90,0x13,0xC6,0x68,0x57,0x4E,0x41,0x8B,0x10,0x5E,0x1D,0xA9,0x44,0xD3,0xBA,0x47,0xB8,0xDD,0xE4,0x35,0x86,0x11,0x93,0x94,0x92,0x5F,0x29,0xC7,0x4C,0x30,0x0C,0x41,0xC5,0x1C,0x3B,0x2E,0xD3,0x05,0x15,0x53,0x6C,0x07,0x4D,0x15,0x14,0x8C,0xB5,0xC9,0x6A,0x44,0x90,0x10,0x4E,0x9A,0xB6,0x21,0x81,0x23,0x3A,0x91,0x91,0xE8,0xFF,0x01};
|
||||||
|
uint8_t spFOURTY[] PROGMEM = {0x04,0x18,0xB6,0x4C,0x00,0xC3,0x56,0x30,0xA0,0xE8,0xF4,0xA0,0x98,0x99,0x62,0x91,0xAE,0x83,0x6B,0x77,0x89,0x78,0x3B,0x09,0xAE,0xBD,0xA6,0x1E,0x63,0x3B,0x79,0x7E,0x71,0x5A,0x8F,0x95,0xE6,0xA5,0x4A,0x69,0xB9,0x4E,0x8A,0x5F,0x12,0x56,0xE4,0x58,0x69,0xE1,0x36,0xA1,0x69,0x2E,0x2B,0xF9,0x95,0x93,0x55,0x17,0xED,0xE4,0x37,0xC6,0xBA,0x93,0xB2,0x92,0xDF,0x19,0xD9,0x6E,0xC8,0x0A,0xFE,0x60,0xE8,0x37,0x21,0xC9,0xF9,0x8D,0x61,0x5F,0x32,0x13,0xE7,0x17,0x4C,0xD3,0xC6,0xB1,0x94,0x97,0x10,0x8F,0x8B,0xAD,0x11,0x7E,0xA1,0x9A,0x26,0x92,0xF6,0xFF,0x01};
|
||||||
|
uint8_t spFIFTY[] PROGMEM = {0x08,0xE8,0x2E,0x84,0x00,0x23,0x84,0x13,0x60,0x38,0x95,0xA5,0x0F,0xCF,0xE2,0x79,0x8A,0x8F,0x37,0x02,0xB3,0xD5,0x2A,0x6E,0x5E,0x93,0x94,0x79,0x45,0xD9,0x05,0x5D,0x0A,0xB9,0x97,0x63,0x02,0x74,0xA7,0x82,0x80,0xEE,0xC3,0x10,0xD0,0x7D,0x28,0x03,0x6E,0x14,0x06,0x70,0xE6,0x0A,0xC9,0x9A,0x4E,0x37,0xD9,0x95,0x51,0xCE,0xBA,0xA2,0x14,0x0C,0x81,0x36,0x1B,0xB2,0x5C,0x30,0x38,0xFA,0x9C,0xC9,0x32,0x41,0xA7,0x18,0x3B,0xA2,0x48,0x04,0x05,0x51,0x4F,0x91,0x6D,0x12,0x04,0x20,0x9B,0x61,0x89,0xFF,0x1F};
|
||||||
|
uint8_t spGOOD[] PROGMEM = {0x0A,0x28,0xCD,0x34,0x20,0xD9,0x1A,0x45,0x74,0xE4,0x66,0x24,0xAD,0xBA,0xB1,0x8C,0x9B,0x91,0xA5,0x64,0xE6,0x98,0x21,0x16,0x0B,0x96,0x9B,0x4C,0xE5,0xFF,0x01};
|
||||||
|
uint8_t spMORNING[] PROGMEM = {0xCE,0x08,0x52,0x2A,0x35,0x5D,0x39,0x53,0x29,0x5B,0xB7,0x0A,0x15,0x0C,0xEE,0x2A,0x42,0x56,0x66,0xD2,0x55,0x2E,0x37,0x2F,0xD9,0x45,0xB3,0xD3,0xC5,0xCA,0x6D,0x27,0xD5,0xEE,0x50,0xF5,0x50,0x94,0x14,0x77,0x2D,0xD8,0x5D,0x49,0x92,0xFD,0xB1,0x64,0x2F,0xA9,0x49,0x0C,0x93,0x4B,0xAD,0x19,0x17,0x3E,0x66,0x1E,0xF1,0xA2,0x5B,0x84,0xE2,0x29,0x8F,0x8B,0x72,0x10,0xB5,0xB1,0x2E,0x4B,0xD4,0x45,0x89,0x4A,0xEC,0x5C,0x95,0x14,0x2B,0x8A,0x9C,0x34,0x52,0x5D,0xBC,0xCC,0xB5,0x3B,0x49,0x69,0x89,0x87,0xC1,0x98,0x56,0x3A,0x21,0x2B,0x82,0x67,0xCC,0x5C,0x85,0xB5,0x4A,0x8A,0xF6,0x64,0xA9,0x96,0xC4,0x69,0x3C,0x52,0x81,0x58,0x1C,0x97,0xF6,0x0E,0x1B,0xCC,0x0D,0x42,0x32,0xAA,0x65,0x12,0x67,0xD4,0x6A,0x61,0x52,0xFC,0xFF};
|
||||||
|
uint8_t spAFTERNOON[] PROGMEM = {0xC7,0xCE,0xCE,0x3A,0xCB,0x58,0x1F,0x3B,0x07,0x9D,0x28,0x71,0xB4,0xAC,0x9C,0x74,0x5A,0x42,0x55,0x33,0xB2,0x93,0x0A,0x09,0xD4,0xC5,0x9A,0xD6,0x44,0x45,0xE3,0x38,0x60,0x9A,0x32,0x05,0xF4,0x18,0x01,0x09,0xD8,0xA9,0xC2,0x00,0x5E,0xCA,0x24,0xD5,0x5B,0x9D,0x4A,0x95,0xEA,0x34,0xEE,0x63,0x92,0x5C,0x4D,0xD0,0xA4,0xEE,0x58,0x0C,0xB9,0x4D,0xCD,0x42,0xA2,0x3A,0x24,0x37,0x25,0x8A,0xA8,0x8E,0xA0,0x53,0xE4,0x28,0x23,0x26,0x13,0x72,0x91,0xA2,0x76,0xBB,0x72,0x38,0x45,0x0A,0x46,0x63,0xCA,0x69,0x27,0x39,0x58,0xB1,0x8D,0x60,0x1C,0x34,0x1B,0x34,0xC3,0x55,0x8E,0x73,0x45,0x2D,0x4F,0x4A,0x3A,0x26,0x10,0xA1,0xCA,0x2D,0xE9,0x98,0x24,0x0A,0x1E,0x6D,0x97,0x29,0xD2,0xCC,0x71,0xA2,0xDC,0x86,0xC8,0x12,0xA7,0x8E,0x08,0x85,0x22,0x8D,0x9C,0x43,0xA7,0x12,0xB2,0x2E,0x50,0x09,0xEF,0x51,0xC5,0xBA,0x28,0x58,0xAD,0xDB,0xE1,0xFF,0x03};
|
||||||
|
uint8_t spEVENING[] PROGMEM = {0xCD,0x6D,0x98,0x73,0x47,0x65,0x0D,0x6D,0x10,0xB2,0x5D,0x93,0x35,0x94,0xC1,0xD0,0x76,0x4D,0x66,0x93,0xA7,0x04,0xBD,0x71,0xD9,0x45,0xAE,0x92,0xD5,0xAC,0x53,0x07,0x6D,0xA5,0x76,0x63,0x51,0x92,0xD4,0xA1,0x83,0xD4,0xCB,0xB2,0x51,0x88,0xCD,0xF5,0x50,0x45,0xCE,0xA2,0x2E,0x27,0x28,0x54,0x15,0x37,0x0A,0xCF,0x75,0x61,0x5D,0xA2,0xC4,0xB5,0xC7,0x44,0x55,0x8A,0x0B,0xA3,0x6E,0x17,0x95,0x21,0xA9,0x0C,0x37,0xCD,0x15,0xBA,0xD4,0x2B,0x6F,0xB3,0x54,0xE4,0xD2,0xC8,0x64,0xBC,0x4C,0x91,0x49,0x12,0xE7,0xB2,0xB1,0xD0,0x22,0x0D,0x9C,0xDD,0xAB,0x62,0xA9,0x38,0x53,0x11,0xA9,0x74,0x2C,0xD2,0xCA,0x59,0x34,0xA3,0xE5,0xFF,0x03};
|
||||||
|
uint8_t spPAUSE1[] PROGMEM = {0x00,0x00,0x00,0x00,0xFF,0x0F};
|
||||||
|
|
||||||
|
void sayTime(int hour, int minutes, AudioGeneratorTalkie *talkie)
|
||||||
|
{
|
||||||
|
bool pm = (hour >= 12);
|
||||||
|
uint8_t *spHour[] = { spTWELVE, spONE, spTWO, spTHREE, spFOUR, spFIVE, spSIX,
|
||||||
|
spSEVEN, spEIGHT, spNINE, spTEN, spELEVEN };
|
||||||
|
size_t spHourLen[] = { sizeof(spTWELVE), sizeof(spONE), sizeof(spTWO),
|
||||||
|
sizeof(spTHREE), sizeof(spFOUR), sizeof(spFIVE),
|
||||||
|
sizeof(spSIX), sizeof(spSEVEN), sizeof(spEIGHT),
|
||||||
|
sizeof(spNINE), sizeof(spTEN), sizeof(spELEVEN) };
|
||||||
|
uint8_t *spMinDec[] = { spOH, spTEN, spTWENTY, spTHIRTY, spFOURTY, spFIFTY };
|
||||||
|
size_t spMinDecLen[] = { sizeof(spOH), sizeof(spTEN), sizeof(spTWENTY),
|
||||||
|
sizeof(spTHIRTY), sizeof(spFOURTY), sizeof(spFIFTY) };
|
||||||
|
uint8_t *spMinSpecial[] = { spELEVEN, spTWELVE, spTHIRTEEN, spFOURTEEN,
|
||||||
|
spFIFTEEN, spSIXTEEN, spSEVENTEEN, spEIGHTEEN,
|
||||||
|
spNINETEEN };
|
||||||
|
size_t spMinSpecialLen[] = { sizeof(spELEVEN), sizeof(spTWELVE),
|
||||||
|
sizeof(spTHIRTEEN), sizeof(spFOURTEEN),
|
||||||
|
sizeof(spFIFTEEN), sizeof(spSIXTEEN),
|
||||||
|
sizeof(spSEVENTEEN), sizeof(spEIGHTEEN),
|
||||||
|
sizeof(spNINETEEN) };
|
||||||
|
uint8_t *spMinLow[] = { spONE, spTWO, spTHREE, spFOUR, spFIVE, spSIX,
|
||||||
|
spSEVEN, spEIGHT, spNINE };
|
||||||
|
size_t spMinLowLen[] = { sizeof(spONE), sizeof(spTWO), sizeof(spTHREE),
|
||||||
|
sizeof(spFOUR), sizeof(spFIVE), sizeof(spSIX),
|
||||||
|
sizeof(spSEVEN), sizeof(spEIGHT), sizeof(spNINE) };
|
||||||
|
|
||||||
|
talkie->say(spTHE, sizeof(spTHE));
|
||||||
|
talkie->say(spTIME, sizeof(spTIME));
|
||||||
|
talkie->say(spIS, sizeof(spIS));
|
||||||
|
|
||||||
|
hour = hour % 12;
|
||||||
|
talkie->say(spHour[hour], spHourLen[hour]);
|
||||||
|
if (minutes==0) {
|
||||||
|
talkie->say(spOCLOCK, sizeof(spOCLOCK));
|
||||||
|
} else if (minutes<=10 || minutes >=20) {
|
||||||
|
talkie->say(spMinDec[minutes / 10], spMinDecLen[minutes /10]);
|
||||||
|
if (minutes % 10) {
|
||||||
|
talkie->say(spMinLow[(minutes % 10) - 1], spMinLowLen[(minutes % 10) - 1]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
talkie->say(spMinSpecial[minutes - 11], spMinSpecialLen[minutes - 11]);
|
||||||
|
}
|
||||||
|
if (pm) {
|
||||||
|
talkie->say(spP_M_, sizeof(spP_M_));
|
||||||
|
} else {
|
||||||
|
talkie->say(spA_M_, sizeof(spA_M_));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
AudioGeneratorTalkie *talkie;
|
||||||
|
AudioOutputI2S *out;
|
||||||
|
|
||||||
|
|
||||||
|
bool GetLocalTime(struct tm * info, uint32_t ms) {
|
||||||
|
uint32_t count = ms / 10;
|
||||||
|
time_t now;
|
||||||
|
|
||||||
|
time(&now);
|
||||||
|
localtime_r(&now, info);
|
||||||
|
|
||||||
|
if (info->tm_year > (2016 - 1900)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (count--) {
|
||||||
|
delay(10);
|
||||||
|
time(&now);
|
||||||
|
localtime_r(&now, info);
|
||||||
|
if (info->tm_year > (2016 - 1900)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup()
|
||||||
|
{
|
||||||
|
Serial.begin(115200);
|
||||||
|
delay(1000);
|
||||||
|
// We start by connecting to a WiFi network
|
||||||
|
Serial.println();
|
||||||
|
Serial.println();
|
||||||
|
Serial.print("Connecting to ");
|
||||||
|
Serial.println(ssid);
|
||||||
|
|
||||||
|
WiFi.begin(ssid, pass);
|
||||||
|
|
||||||
|
while (WiFi.status() != WL_CONNECTED) {
|
||||||
|
delay(500);
|
||||||
|
Serial.print(".");
|
||||||
|
}
|
||||||
|
Serial.println("WiFi connected");
|
||||||
|
Serial.println("IP address: ");
|
||||||
|
Serial.println(WiFi.localIP());
|
||||||
|
Serial.println("Contacting Time Server");
|
||||||
|
configTime(3600 * timezone, daysavetime * 3600, "time.nist.gov", "0.pool.ntp.org", "1.pool.ntp.org");
|
||||||
|
struct tm tmstruct ;
|
||||||
|
do {
|
||||||
|
tmstruct.tm_year = 0;
|
||||||
|
Serial.printf(".");
|
||||||
|
GetLocalTime(&tmstruct, 5000);
|
||||||
|
delay(100);
|
||||||
|
} while (tmstruct.tm_year < 100);
|
||||||
|
|
||||||
|
audioLogger = &Serial;
|
||||||
|
out = new AudioOutputI2S();
|
||||||
|
talkie = new AudioGeneratorTalkie();
|
||||||
|
talkie->begin(nullptr, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop()
|
||||||
|
{
|
||||||
|
struct tm tmstruct ;
|
||||||
|
tmstruct.tm_year = 0;
|
||||||
|
GetLocalTime(&tmstruct, 5000);
|
||||||
|
Serial.printf("\nNow is : %d-%02d-%02d %02d:%02d:%02d\n",
|
||||||
|
tmstruct.tm_year + 1900, tmstruct.tm_mon + 1,
|
||||||
|
tmstruct.tm_mday, tmstruct.tm_hour,
|
||||||
|
tmstruct.tm_min, tmstruct.tm_sec);
|
||||||
|
sayTime(tmstruct.tm_hour, tmstruct.tm_min, talkie);
|
||||||
|
delay(1000);
|
||||||
|
}
|
|
@ -0,0 +1,441 @@
|
||||||
|
/*
|
||||||
|
WebRadio Example
|
||||||
|
Very simple HTML app to control web streaming
|
||||||
|
|
||||||
|
Copyright (C) 2017 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#ifdef ESP32
|
||||||
|
#include <WiFi.h>
|
||||||
|
#else
|
||||||
|
#include <ESP8266WiFi.h>
|
||||||
|
#endif
|
||||||
|
#include "AudioFileSourceICYStream.h"
|
||||||
|
#include "AudioFileSourceBuffer.h"
|
||||||
|
#include "AudioGeneratorMP3.h"
|
||||||
|
#include "AudioGeneratorAAC.h"
|
||||||
|
#include "AudioOutputI2S.h"
|
||||||
|
#include <EEPROM.h>
|
||||||
|
|
||||||
|
// Custom web server that doesn't need much RAM
|
||||||
|
#include "web.h"
|
||||||
|
|
||||||
|
// To run, set your ESP8266 build to 160MHz, update the SSID info, and upload.
|
||||||
|
|
||||||
|
// Enter your WiFi setup here:
|
||||||
|
#ifndef STASSID
|
||||||
|
#define STASSID "your-ssid"
|
||||||
|
#define STAPSK "your-password"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
const char* ssid = STASSID;
|
||||||
|
const char* password = STAPSK;
|
||||||
|
|
||||||
|
WiFiServer server(80);
|
||||||
|
|
||||||
|
AudioGenerator *decoder = NULL;
|
||||||
|
AudioFileSourceICYStream *file = NULL;
|
||||||
|
AudioFileSourceBuffer *buff = NULL;
|
||||||
|
AudioOutputI2S *out = NULL;
|
||||||
|
|
||||||
|
int volume = 100;
|
||||||
|
char title[64];
|
||||||
|
char url[96];
|
||||||
|
char status[64];
|
||||||
|
bool newUrl = false;
|
||||||
|
bool isAAC = false;
|
||||||
|
int retryms = 0;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char url[96];
|
||||||
|
bool isAAC;
|
||||||
|
int16_t volume;
|
||||||
|
int16_t checksum;
|
||||||
|
} Settings;
|
||||||
|
|
||||||
|
// C++11 multiline string constants are neato...
|
||||||
|
static const char HEAD[] PROGMEM = R"KEWL(
|
||||||
|
<head>
|
||||||
|
<title>ESP8266 Web Radio</title>
|
||||||
|
<script type="text/javascript">
|
||||||
|
function updateTitle() {
|
||||||
|
var x = new XMLHttpRequest();
|
||||||
|
x.open("GET", "title");
|
||||||
|
x.onload = function() { document.getElementById("titlespan").innerHTML=x.responseText; setTimeout(updateTitle, 5000); }
|
||||||
|
x.onerror = function() { setTimeout(updateTitle, 5000); }
|
||||||
|
x.send();
|
||||||
|
}
|
||||||
|
setTimeout(updateTitle, 1000);
|
||||||
|
function showValue(n) {
|
||||||
|
document.getElementById("volspan").innerHTML=n;
|
||||||
|
var x = new XMLHttpRequest();
|
||||||
|
x.open("GET", "setvol?vol="+n);
|
||||||
|
x.send();
|
||||||
|
}
|
||||||
|
function updateStatus() {var x = new XMLHttpRequest();
|
||||||
|
x.open("GET", "status");
|
||||||
|
x.onload = function() { document.getElementById("statusspan").innerHTML=x.responseText; setTimeout(updateStatus, 5000); }
|
||||||
|
x.onerror = function() { setTimeout(updateStatus, 5000); }
|
||||||
|
x.send();
|
||||||
|
}
|
||||||
|
setTimeout(updateStatus, 2000);
|
||||||
|
</script>
|
||||||
|
</head>)KEWL";
|
||||||
|
|
||||||
|
static const char BODY[] PROGMEM = R"KEWL(
|
||||||
|
<body>
|
||||||
|
ESP8266 Web Radio!
|
||||||
|
<hr>
|
||||||
|
Currently Playing: <span id="titlespan">%s</span><br>
|
||||||
|
Volume: <input type="range" name="vol" min="1" max="150" steps="10" value="%d" onchange="showValue(this.value)"/> <span id="volspan">%d</span>%%
|
||||||
|
<hr>
|
||||||
|
Status: <span id="statusspan">%s</span>
|
||||||
|
<hr>
|
||||||
|
<form action="changeurl" method="GET">
|
||||||
|
Current URL: %s<br>
|
||||||
|
Change URL: <input type="text" name="url">
|
||||||
|
<select name="type"><option value="mp3">MP3</option><option value="aac">AAC</option></select>
|
||||||
|
<input type="submit" value="Change"></form>
|
||||||
|
<form action="stop" method="POST"><input type="submit" value="Stop"></form>
|
||||||
|
</body>)KEWL";
|
||||||
|
|
||||||
|
void HandleIndex(WiFiClient *client)
|
||||||
|
{
|
||||||
|
char buff[sizeof(BODY) + sizeof(title) + sizeof(status) + sizeof(url) + 3*2];
|
||||||
|
|
||||||
|
Serial.printf_P(PSTR("Sending INDEX...Free mem=%d\n"), ESP.getFreeHeap());
|
||||||
|
WebHeaders(client, NULL);
|
||||||
|
WebPrintf(client, DOCTYPE);
|
||||||
|
client->write_P( PSTR("<html>"), 6 );
|
||||||
|
client->write_P( HEAD, strlen_P(HEAD) );
|
||||||
|
sprintf_P(buff, BODY, title, volume, volume, status, url);
|
||||||
|
client->write(buff, strlen(buff) );
|
||||||
|
client->write_P( PSTR("</html>"), 7 );
|
||||||
|
Serial.printf_P(PSTR("Sent INDEX...Free mem=%d\n"), ESP.getFreeHeap());
|
||||||
|
}
|
||||||
|
|
||||||
|
void HandleStatus(WiFiClient *client)
|
||||||
|
{
|
||||||
|
WebHeaders(client, NULL);
|
||||||
|
client->write(status, strlen(status));
|
||||||
|
}
|
||||||
|
|
||||||
|
void HandleTitle(WiFiClient *client)
|
||||||
|
{
|
||||||
|
WebHeaders(client, NULL);
|
||||||
|
client->write(title, strlen(title));
|
||||||
|
}
|
||||||
|
|
||||||
|
void HandleVolume(WiFiClient *client, char *params)
|
||||||
|
{
|
||||||
|
char *namePtr;
|
||||||
|
char *valPtr;
|
||||||
|
|
||||||
|
while (ParseParam(¶ms, &namePtr, &valPtr)) {
|
||||||
|
ParamInt("vol", volume);
|
||||||
|
}
|
||||||
|
Serial.printf_P(PSTR("Set volume: %d\n"), volume);
|
||||||
|
out->SetGain(((float)volume)/100.0);
|
||||||
|
RedirectToIndex(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HandleChangeURL(WiFiClient *client, char *params)
|
||||||
|
{
|
||||||
|
char *namePtr;
|
||||||
|
char *valPtr;
|
||||||
|
char newURL[sizeof(url)];
|
||||||
|
char newType[4];
|
||||||
|
|
||||||
|
newURL[0] = 0;
|
||||||
|
newType[0] = 0;
|
||||||
|
while (ParseParam(¶ms, &namePtr, &valPtr)) {
|
||||||
|
ParamText("url", newURL);
|
||||||
|
ParamText("type", newType);
|
||||||
|
}
|
||||||
|
if (newURL[0] && newType[0]) {
|
||||||
|
newUrl = true;
|
||||||
|
strncpy(url, newURL, sizeof(url)-1);
|
||||||
|
url[sizeof(url)-1] = 0;
|
||||||
|
if (!strcmp_P(newType, PSTR("aac"))) {
|
||||||
|
isAAC = true;
|
||||||
|
} else {
|
||||||
|
isAAC = false;
|
||||||
|
}
|
||||||
|
strcpy_P(status, PSTR("Changing URL..."));
|
||||||
|
Serial.printf_P(PSTR("Changed URL to: %s(%s)\n"), url, newType);
|
||||||
|
RedirectToIndex(client);
|
||||||
|
} else {
|
||||||
|
WebError(client, 404, NULL, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RedirectToIndex(WiFiClient *client)
|
||||||
|
{
|
||||||
|
WebError(client, 301, PSTR("Location: /\r\n"), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void StopPlaying()
|
||||||
|
{
|
||||||
|
if (decoder) {
|
||||||
|
decoder->stop();
|
||||||
|
delete decoder;
|
||||||
|
decoder = NULL;
|
||||||
|
}
|
||||||
|
if (buff) {
|
||||||
|
buff->close();
|
||||||
|
delete buff;
|
||||||
|
buff = NULL;
|
||||||
|
}
|
||||||
|
if (file) {
|
||||||
|
file->close();
|
||||||
|
delete file;
|
||||||
|
file = NULL;
|
||||||
|
}
|
||||||
|
strcpy_P(status, PSTR("Stopped"));
|
||||||
|
strcpy_P(title, PSTR("Stopped"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void HandleStop(WiFiClient *client)
|
||||||
|
{
|
||||||
|
Serial.printf_P(PSTR("HandleStop()\n"));
|
||||||
|
StopPlaying();
|
||||||
|
RedirectToIndex(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MDCallback(void *cbData, const char *type, bool isUnicode, const char *str)
|
||||||
|
{
|
||||||
|
const char *ptr = reinterpret_cast<const char *>(cbData);
|
||||||
|
(void) isUnicode; // Punt this ball for now
|
||||||
|
(void) ptr;
|
||||||
|
if (strstr_P(type, PSTR("Title"))) {
|
||||||
|
strncpy(title, str, sizeof(title));
|
||||||
|
title[sizeof(title)-1] = 0;
|
||||||
|
} else {
|
||||||
|
// Who knows what to do? Not me!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void StatusCallback(void *cbData, int code, const char *string)
|
||||||
|
{
|
||||||
|
const char *ptr = reinterpret_cast<const char *>(cbData);
|
||||||
|
(void) code;
|
||||||
|
(void) ptr;
|
||||||
|
strncpy_P(status, string, sizeof(status)-1);
|
||||||
|
status[sizeof(status)-1] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef ESP8266
|
||||||
|
const int preallocateBufferSize = 5*1024;
|
||||||
|
const int preallocateCodecSize = 29192; // MP3 codec max mem needed
|
||||||
|
#else
|
||||||
|
const int preallocateBufferSize = 16*1024;
|
||||||
|
const int preallocateCodecSize = 85332; // AAC+SBR codec max mem needed
|
||||||
|
#endif
|
||||||
|
void *preallocateBuffer = NULL;
|
||||||
|
void *preallocateCodec = NULL;
|
||||||
|
|
||||||
|
void setup()
|
||||||
|
{
|
||||||
|
// First, preallocate all the memory needed for the buffering and codecs, never to be freed
|
||||||
|
preallocateBuffer = malloc(preallocateBufferSize);
|
||||||
|
preallocateCodec = malloc(preallocateCodecSize);
|
||||||
|
if (!preallocateBuffer || !preallocateCodec) {
|
||||||
|
Serial.begin(115200);
|
||||||
|
Serial.printf_P(PSTR("FATAL ERROR: Unable to preallocate %d bytes for app\n"), preallocateBufferSize+preallocateCodecSize);
|
||||||
|
while (1) delay(1000); // Infinite halt
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.begin(115200);
|
||||||
|
|
||||||
|
delay(1000);
|
||||||
|
Serial.printf_P(PSTR("Connecting to WiFi\n"));
|
||||||
|
|
||||||
|
WiFi.disconnect();
|
||||||
|
WiFi.softAPdisconnect(true);
|
||||||
|
WiFi.mode(WIFI_STA);
|
||||||
|
|
||||||
|
WiFi.begin(ssid, password);
|
||||||
|
|
||||||
|
// Try forever
|
||||||
|
while (WiFi.status() != WL_CONNECTED) {
|
||||||
|
Serial.printf_P(PSTR("...Connecting to WiFi\n"));
|
||||||
|
delay(1000);
|
||||||
|
}
|
||||||
|
Serial.printf_P(PSTR("Connected\n"));
|
||||||
|
|
||||||
|
Serial.printf_P(PSTR("Go to http://"));
|
||||||
|
Serial.print(WiFi.localIP());
|
||||||
|
Serial.printf_P(PSTR("/ to control the web radio.\n"));
|
||||||
|
|
||||||
|
server.begin();
|
||||||
|
|
||||||
|
strcpy_P(url, PSTR("none"));
|
||||||
|
strcpy_P(status, PSTR("OK"));
|
||||||
|
strcpy_P(title, PSTR("Idle"));
|
||||||
|
|
||||||
|
audioLogger = &Serial;
|
||||||
|
file = NULL;
|
||||||
|
buff = NULL;
|
||||||
|
out = new AudioOutputI2S();
|
||||||
|
decoder = NULL;
|
||||||
|
|
||||||
|
LoadSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
void StartNewURL()
|
||||||
|
{
|
||||||
|
Serial.printf_P(PSTR("Changing URL to: %s, vol=%d\n"), url, volume);
|
||||||
|
|
||||||
|
newUrl = false;
|
||||||
|
// Stop and free existing ones
|
||||||
|
Serial.printf_P(PSTR("Before stop...Free mem=%d\n"), ESP.getFreeHeap());
|
||||||
|
StopPlaying();
|
||||||
|
Serial.printf_P(PSTR("After stop...Free mem=%d\n"), ESP.getFreeHeap());
|
||||||
|
SaveSettings();
|
||||||
|
Serial.printf_P(PSTR("Saved settings\n"));
|
||||||
|
|
||||||
|
file = new AudioFileSourceICYStream(url);
|
||||||
|
Serial.printf_P(PSTR("created icystream\n"));
|
||||||
|
file->RegisterMetadataCB(MDCallback, NULL);
|
||||||
|
buff = new AudioFileSourceBuffer(file, preallocateBuffer, preallocateBufferSize);
|
||||||
|
Serial.printf_P(PSTR("created buffer\n"));
|
||||||
|
buff->RegisterStatusCB(StatusCallback, NULL);
|
||||||
|
decoder = isAAC ? (AudioGenerator*) new AudioGeneratorAAC(preallocateCodec, preallocateCodecSize) : (AudioGenerator*) new AudioGeneratorMP3(preallocateCodec, preallocateCodecSize);
|
||||||
|
Serial.printf_P(PSTR("created decoder\n"));
|
||||||
|
decoder->RegisterStatusCB(StatusCallback, NULL);
|
||||||
|
Serial.printf_P("Decoder start...\n");
|
||||||
|
decoder->begin(buff, out);
|
||||||
|
out->SetGain(((float)volume)/100.0);
|
||||||
|
if (!decoder->isRunning()) {
|
||||||
|
Serial.printf_P(PSTR("Can't connect to URL"));
|
||||||
|
StopPlaying();
|
||||||
|
strcpy_P(status, PSTR("Unable to connect to URL"));
|
||||||
|
retryms = millis() + 2000;
|
||||||
|
}
|
||||||
|
Serial.printf_P("Done start new URL\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
void LoadSettings()
|
||||||
|
{
|
||||||
|
// Restore from EEPROM, check the checksum matches
|
||||||
|
Settings s;
|
||||||
|
uint8_t *ptr = reinterpret_cast<uint8_t *>(&s);
|
||||||
|
EEPROM.begin(sizeof(s));
|
||||||
|
for (size_t i=0; i<sizeof(s); i++) {
|
||||||
|
ptr[i] = EEPROM.read(i);
|
||||||
|
}
|
||||||
|
EEPROM.end();
|
||||||
|
int16_t sum = 0x1234;
|
||||||
|
for (size_t i=0; i<sizeof(url); i++) sum += s.url[i];
|
||||||
|
sum += s.isAAC;
|
||||||
|
sum += s.volume;
|
||||||
|
if (s.checksum == sum) {
|
||||||
|
strcpy(url, s.url);
|
||||||
|
isAAC = s.isAAC;
|
||||||
|
volume = s.volume;
|
||||||
|
Serial.printf_P(PSTR("Resuming stream from EEPROM: %s, type=%s, vol=%d\n"), url, isAAC?"AAC":"MP3", volume);
|
||||||
|
newUrl = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SaveSettings()
|
||||||
|
{
|
||||||
|
// Store in "EEPROM" to restart automatically
|
||||||
|
Settings s;
|
||||||
|
memset(&s, 0, sizeof(s));
|
||||||
|
strcpy(s.url, url);
|
||||||
|
s.isAAC = isAAC;
|
||||||
|
s.volume = volume;
|
||||||
|
s.checksum = 0x1234;
|
||||||
|
for (size_t i=0; i<sizeof(url); i++) s.checksum += s.url[i];
|
||||||
|
s.checksum += s.isAAC;
|
||||||
|
s.checksum += s.volume;
|
||||||
|
uint8_t *ptr = reinterpret_cast<uint8_t *>(&s);
|
||||||
|
EEPROM.begin(sizeof(s));
|
||||||
|
for (size_t i=0; i<sizeof(s); i++) {
|
||||||
|
EEPROM.write(i, ptr[i]);
|
||||||
|
}
|
||||||
|
EEPROM.commit();
|
||||||
|
EEPROM.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
void PumpDecoder()
|
||||||
|
{
|
||||||
|
if (decoder && decoder->isRunning()) {
|
||||||
|
strcpy_P(status, PSTR("Playing")); // By default we're OK unless the decoder says otherwise
|
||||||
|
if (!decoder->loop()) {
|
||||||
|
Serial.printf_P(PSTR("Stopping decoder\n"));
|
||||||
|
StopPlaying();
|
||||||
|
retryms = millis() + 2000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop()
|
||||||
|
{
|
||||||
|
static int lastms = 0;
|
||||||
|
if (millis()-lastms > 1000) {
|
||||||
|
lastms = millis();
|
||||||
|
Serial.printf_P(PSTR("Running for %d seconds%c...Free mem=%d\n"), lastms/1000, !decoder?' ':(decoder->isRunning()?'*':' '), ESP.getFreeHeap());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (retryms && millis()-retryms>0) {
|
||||||
|
retryms = 0;
|
||||||
|
newUrl = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newUrl) {
|
||||||
|
StartNewURL();
|
||||||
|
}
|
||||||
|
|
||||||
|
PumpDecoder();
|
||||||
|
|
||||||
|
char *reqUrl;
|
||||||
|
char *params;
|
||||||
|
WiFiClient client = server.available();
|
||||||
|
PumpDecoder();
|
||||||
|
char reqBuff[384];
|
||||||
|
if (client && WebReadRequest(&client, reqBuff, 384, &reqUrl, ¶ms)) {
|
||||||
|
PumpDecoder();
|
||||||
|
if (IsIndexHTML(reqUrl)) {
|
||||||
|
HandleIndex(&client);
|
||||||
|
} else if (!strcmp_P(reqUrl, PSTR("stop"))) {
|
||||||
|
HandleStop(&client);
|
||||||
|
} else if (!strcmp_P(reqUrl, PSTR("status"))) {
|
||||||
|
HandleStatus(&client);
|
||||||
|
} else if (!strcmp_P(reqUrl, PSTR("title"))) {
|
||||||
|
HandleTitle(&client);
|
||||||
|
} else if (!strcmp_P(reqUrl, PSTR("setvol"))) {
|
||||||
|
HandleVolume(&client, params);
|
||||||
|
} else if (!strcmp_P(reqUrl, PSTR("changeurl"))) {
|
||||||
|
HandleChangeURL(&client, params);
|
||||||
|
} else {
|
||||||
|
WebError(&client, 404, NULL, false);
|
||||||
|
}
|
||||||
|
// web clients hate when door is violently shut
|
||||||
|
while (client.available()) {
|
||||||
|
PumpDecoder();
|
||||||
|
client.read();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PumpDecoder();
|
||||||
|
if (client) {
|
||||||
|
client.flush();
|
||||||
|
client.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,314 @@
|
||||||
|
/*
|
||||||
|
PsychoPlug
|
||||||
|
ESP8266 based remote outlet with standalone timer and MQTT integration
|
||||||
|
|
||||||
|
Copyright (C) 2017 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#ifdef ESP32
|
||||||
|
#include <WiFi.h>
|
||||||
|
#else
|
||||||
|
#include <ESP8266WiFi.h>
|
||||||
|
#endif
|
||||||
|
#include "web.h"
|
||||||
|
|
||||||
|
void WebPrintError(WiFiClient *client, int code)
|
||||||
|
{
|
||||||
|
switch(code) {
|
||||||
|
case 301: WebPrintf(client, "301 Moved Permanently"); break;
|
||||||
|
case 400: WebPrintf(client, "400 Bad Request"); break;
|
||||||
|
case 401: WebPrintf(client, "401 Unauthorized"); break;
|
||||||
|
case 404: WebPrintf(client, "404 Not Found"); break;
|
||||||
|
case 405: WebPrintf(client, "405 Method Not Allowed"); break;
|
||||||
|
default: WebPrintf(client, "500 Server Error"); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void WebError(WiFiClient *client, int code, const char *headers, bool usePMEM)
|
||||||
|
{
|
||||||
|
WebPrintf(client, "HTTP/1.1 %d\r\n", code);
|
||||||
|
WebPrintf(client, "Server: PsychoPlug\r\n");
|
||||||
|
WebPrintf(client, "Content-type: text/html\r\n");
|
||||||
|
WebPrintf(client, "Cache-Control: no-cache, no-store, must-revalidate\r\n");
|
||||||
|
WebPrintf(client, "Pragma: no-cache\r\n");
|
||||||
|
WebPrintf(client, "Expires: 0\r\n");
|
||||||
|
WebPrintf(client, "Connection: close\r\n");
|
||||||
|
if (headers) {
|
||||||
|
if (!usePMEM) {
|
||||||
|
WebPrintf(client, "%s", headers);
|
||||||
|
} else {
|
||||||
|
WebPrintfPSTR(client, headers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WebPrintf(client, "\r\n\r\n");
|
||||||
|
WebPrintf(client, DOCTYPE);
|
||||||
|
WebPrintf(client, "<html><head><title>");
|
||||||
|
WebPrintError(client, code);
|
||||||
|
WebPrintf(client, "</title>" ENCODING "</head>\n");
|
||||||
|
WebPrintf(client, "<body><h1>");
|
||||||
|
WebPrintError(client, code);
|
||||||
|
WebPrintf(client, "</h1></body></html>\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
void WebHeaders(WiFiClient *client, PGM_P /*const char **/headers)
|
||||||
|
{
|
||||||
|
WebPrintf(client, "HTTP/1.1 200 OK\r\n");
|
||||||
|
WebPrintf(client, "Server: PsychoPlug\r\n");
|
||||||
|
WebPrintf(client, "Content-type: text/html\r\n");
|
||||||
|
WebPrintf(client, "Cache-Control: no-cache, no-store, must-revalidate\r\n");
|
||||||
|
WebPrintf(client, "Pragma: no-cache\r\n");
|
||||||
|
WebPrintf(client, "Connection: close\r\n");
|
||||||
|
WebPrintf(client, "Expires: 0\r\n");
|
||||||
|
if (headers) {
|
||||||
|
WebPrintfPSTR(client, headers);
|
||||||
|
}
|
||||||
|
WebPrintf(client, "\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// In-place decoder, overwrites source with decoded values. Needs 0-termination on input
|
||||||
|
// Try and keep memory needs low, speed not critical
|
||||||
|
static uint8_t b64lut(uint8_t i)
|
||||||
|
{
|
||||||
|
if (i >= 'A' && i <= 'Z') return i - 'A';
|
||||||
|
if (i >= 'a' && i <= 'z') return i - 'a' + 26;
|
||||||
|
if (i >= '0' && i <= '9') return i - '0' + 52;
|
||||||
|
if (i == '-') return 62;
|
||||||
|
if (i == '_') return 63;
|
||||||
|
else return 64;// sentinel
|
||||||
|
}
|
||||||
|
|
||||||
|
void Base64Decode(char *str)
|
||||||
|
{
|
||||||
|
char *dest;
|
||||||
|
dest = str;
|
||||||
|
|
||||||
|
if (strlen(str)%4) return; // Not multiple of 4 == error
|
||||||
|
|
||||||
|
while (*str) {
|
||||||
|
uint8_t a = b64lut(*(str++));
|
||||||
|
uint8_t b = b64lut(*(str++));
|
||||||
|
uint8_t c = b64lut(*(str++));
|
||||||
|
uint8_t d = b64lut(*(str++));
|
||||||
|
*(dest++) = (a << 2) | ((b & 0x30) >> 4);
|
||||||
|
if (c == 64) break;
|
||||||
|
*(dest++) = ((b & 0x0f) << 4) | ((c & 0x3c) >> 2);
|
||||||
|
if (d == 64) break;
|
||||||
|
*(dest++) = ((c & 0x03) << 6) | d;
|
||||||
|
}
|
||||||
|
*dest = 0; // Terminate the string
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void URLDecode(char *ptr)
|
||||||
|
{
|
||||||
|
while (*ptr) {
|
||||||
|
if (*ptr == '+') {
|
||||||
|
*ptr = ' ';
|
||||||
|
} else if (*ptr == '%') {
|
||||||
|
if (*(ptr+1) && *(ptr+2)) {
|
||||||
|
byte a = *(ptr + 1);
|
||||||
|
byte b = *(ptr + 2);
|
||||||
|
if (a>='0' && a<='9') a -= '0';
|
||||||
|
else if (a>='a' && a<='f') a = a - 'a' + 10;
|
||||||
|
else if (a>='A' && a<='F') a = a - 'A' + 10;
|
||||||
|
if (b>='0' && b<='9') b -= '0';
|
||||||
|
else if (b>='a' && b<='f') b = b - 'a' + 10;
|
||||||
|
else if (b>='A' && b<='F') b = b - 'A' + 10;
|
||||||
|
*ptr = ((a&0x0f)<<4) | (b&0x0f);
|
||||||
|
// Safe strcpy the rest of the string back
|
||||||
|
char *p1 = ptr + 1;
|
||||||
|
char *p2 = ptr + 3;
|
||||||
|
while (*p2) { *p1 = *p2; p1++; p2++; }
|
||||||
|
*p1 = 0;
|
||||||
|
}
|
||||||
|
// OTW this is a bad encoding, just pass unchanged
|
||||||
|
}
|
||||||
|
ptr++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Parse HTTP request
|
||||||
|
bool WebReadRequest(WiFiClient *client, char *reqBuff, int reqBuffLen, char **urlStr, char **paramStr)
|
||||||
|
{
|
||||||
|
static char NUL = 0; // Get around writable strings...
|
||||||
|
|
||||||
|
*urlStr = NULL;
|
||||||
|
*paramStr = NULL;
|
||||||
|
|
||||||
|
unsigned long timeoutMS = millis() + 5000; // Max delay before we timeout
|
||||||
|
while (!client->available() && millis() < timeoutMS) { delay(10); }
|
||||||
|
if (!client->available()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int wlen = client->readBytesUntil('\r', reqBuff, reqBuffLen-1);
|
||||||
|
reqBuff[wlen] = 0;
|
||||||
|
|
||||||
|
|
||||||
|
// Delete HTTP version (well, anything after the 2nd space)
|
||||||
|
char *ptr = reqBuff;
|
||||||
|
while (*ptr && *ptr!=' ') ptr++;
|
||||||
|
if (*ptr) ptr++;
|
||||||
|
while (*ptr && *ptr!=' ') ptr++;
|
||||||
|
*ptr = 0;
|
||||||
|
|
||||||
|
URLDecode(reqBuff);
|
||||||
|
|
||||||
|
char *url;
|
||||||
|
char *qp;
|
||||||
|
if (!memcmp_P(reqBuff, PSTR("GET "), 4)) {
|
||||||
|
client->flush(); // Don't need anything here...
|
||||||
|
|
||||||
|
// Break into URL and form data
|
||||||
|
url = reqBuff+4;
|
||||||
|
while (*url && *url=='/') url++; // Strip off leading /s
|
||||||
|
qp = strchr(url, '?');
|
||||||
|
if (qp) {
|
||||||
|
*qp = 0; // End URL
|
||||||
|
qp++;
|
||||||
|
} else {
|
||||||
|
qp = &NUL;
|
||||||
|
}
|
||||||
|
} else if (!memcmp_P(reqBuff, PSTR("POST "), 5)) {
|
||||||
|
uint8_t newline;
|
||||||
|
client->read(&newline, 1); // Get rid of \n
|
||||||
|
|
||||||
|
url = reqBuff+5;
|
||||||
|
while (*url && *url=='/') url++; // Strip off leading /s
|
||||||
|
qp = strchr(url, '?');
|
||||||
|
if (qp) *qp = 0; // End URL @ ?
|
||||||
|
// In a POST the params are in the body
|
||||||
|
int sizeleft = reqBuffLen - strlen(reqBuff) - 1;
|
||||||
|
qp = reqBuff + strlen(reqBuff) + 1;
|
||||||
|
int wlen = client->readBytesUntil('\r', qp, sizeleft-1);
|
||||||
|
qp[wlen] = 0;
|
||||||
|
client->flush();
|
||||||
|
URLDecode(qp);
|
||||||
|
} else {
|
||||||
|
// Not a GET or POST, error
|
||||||
|
WebError(client, 405, PSTR("Allow: GET, POST"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (urlStr) *urlStr = url;
|
||||||
|
if (paramStr) *paramStr = qp;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Scan out and update a pointeinto the param string, returning the name and value or false if done
|
||||||
|
bool ParseParam(char **paramStr, char **name, char **value)
|
||||||
|
{
|
||||||
|
char *data = *paramStr;
|
||||||
|
|
||||||
|
if (*data==0) return false;
|
||||||
|
|
||||||
|
char *namePtr = data;
|
||||||
|
while ((*data != 0) && (*data != '=') && (*data != '&')) data++;
|
||||||
|
if (*data) { *data = 0; data++; }
|
||||||
|
char *valPtr = data;
|
||||||
|
if (*data == '=') data++;
|
||||||
|
while ((*data != 0) && (*data != '=') && (*data != '&')) data++;
|
||||||
|
if (*data) { *data = 0; data++;}
|
||||||
|
|
||||||
|
*paramStr = data;
|
||||||
|
*name = namePtr;
|
||||||
|
*value = valPtr;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsIndexHTML(const char *url)
|
||||||
|
{
|
||||||
|
if (!url) return false;
|
||||||
|
if (*url==0 || !strcmp_P(url, PSTR("/")) || !strcmp_P(url, PSTR("/index.html")) || !strcmp_P(url, PSTR("index.html"))) return true;
|
||||||
|
else return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
void WebFormText(WiFiClient *client, /*const char **/ PGM_P label, const char *name, const char *value, bool enabled)
|
||||||
|
{
|
||||||
|
WebPrintfPSTR(client, label);
|
||||||
|
WebPrintf(client, ": <input type=\"text\" name=\"%s\" id=\"%s\" value=\"%s\" %s><br>\n", name, name, value, !enabled?"disabled":"");
|
||||||
|
}
|
||||||
|
void WebFormText(WiFiClient *client, /*const char **/ PGM_P label, const char *name, const int value, bool enabled)
|
||||||
|
{
|
||||||
|
WebPrintfPSTR(client, label);
|
||||||
|
WebPrintf(client, ": <input type=\"text\" name=\"%s\" id=\"%s\" value=\"%d\" %s><br>\n", name, name, value, !enabled?"disabled":"");
|
||||||
|
}
|
||||||
|
void WebFormCheckbox(WiFiClient *client, /*const char **/ PGM_P label, const char *name, bool checked, bool enabled)
|
||||||
|
{
|
||||||
|
WebPrintf(client, "<input type=\"checkbox\" name=\"%s\" id=\"%s\" %s %s> ", name, name, checked?"checked":"", !enabled?"disabled":"");
|
||||||
|
WebPrintfPSTR(client, label);
|
||||||
|
WebPrintf(client, "<br>\n");
|
||||||
|
}
|
||||||
|
void WebFormCheckboxDisabler(WiFiClient *client, PGM_P /*const char **/label, const char *name, bool invert, bool checked, bool enabled, const char *ids[])
|
||||||
|
{
|
||||||
|
WebPrintf(client,"<input type=\"checkbox\" name=\"%s\" id=\"%s\" onclick=\"", name,name);
|
||||||
|
if (invert) WebPrintf(client, "var x = true; if (this.checked) { x = false; }\n")
|
||||||
|
else WebPrintf(client, "var x = false; if (this.checked) { x = true; }\n");
|
||||||
|
for (byte i=0; ids[i][0]; i++ ) {
|
||||||
|
WebPrintf(client, "document.getElementById('%s').disabled = x;\n", ids[i]);
|
||||||
|
}
|
||||||
|
WebPrintf(client, "\" %s %s> ", checked?"checked":"", !enabled?"disabled":"")
|
||||||
|
WebPrintfPSTR(client, label);
|
||||||
|
WebPrintf(client, "<br>\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan an integer from a string, place it into dest, and then return # of bytes scanned
|
||||||
|
int ParseInt(char *src, int *dest)
|
||||||
|
{
|
||||||
|
byte count = 0;
|
||||||
|
bool neg = false;
|
||||||
|
int res = 0;
|
||||||
|
if (!src) return 0;
|
||||||
|
if (src[0] == '-') {neg = true; src++; count++;}
|
||||||
|
while (*src && (*src>='0') && (*src<='9')) {
|
||||||
|
res = res * 10;
|
||||||
|
res += *src - '0';
|
||||||
|
src++;
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
if (neg) res *= -1;
|
||||||
|
if (dest) *dest = res;
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Read4Int(char *str, byte *p)
|
||||||
|
{
|
||||||
|
int i;
|
||||||
|
str += ParseInt(str, &i); p[0] = i; if (*str) str++;
|
||||||
|
str += ParseInt(str, &i); p[1] = i; if (*str) str++;
|
||||||
|
str += ParseInt(str, &i); p[2] = i; if (*str) str++;
|
||||||
|
str += ParseInt(str, &i); p[3] = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
/*
|
||||||
|
PsychoPlug
|
||||||
|
ESP8266 based remote outlet with standalone timer and MQTT integration
|
||||||
|
|
||||||
|
Copyright (C) 2017 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _web_h
|
||||||
|
#define _web_h
|
||||||
|
|
||||||
|
// Global way of writing out dynamic HTML to socket
|
||||||
|
// snprintf guarantees a null termination
|
||||||
|
#define WebPrintf(c, fmt, ...) { char webBuff[192]; snprintf_P(webBuff, sizeof(webBuff), PSTR(fmt), ## __VA_ARGS__); (c)->print(webBuff); delay(10);}
|
||||||
|
#define WebPrintfPSTR(c, fmt, ...) { char webBuff[192]; snprintf_P(webBuff, sizeof(webBuff), (fmt), ## __VA_ARGS__); (c)->print(webBuff); delay(10);}
|
||||||
|
|
||||||
|
// Common HTTP header bits
|
||||||
|
#define DOCTYPE "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\">"
|
||||||
|
#define ENCODING "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"/>\n"
|
||||||
|
|
||||||
|
|
||||||
|
// Web header creation
|
||||||
|
void WebPrintError(WiFiClient *client, int code); // Sends only the error code string and a description
|
||||||
|
void WebError(WiFiClient *client, int code, const char *headers, bool usePMEM = true); // Sends whole HTTP error headers
|
||||||
|
void WebHeaders(WiFiClient *client, PGM_P /*const char **/headers); // Send success headers
|
||||||
|
|
||||||
|
// Web decoding utilities
|
||||||
|
void Base64Decode(char *str); // In-place B64 decode
|
||||||
|
void URLDecode(char *ptr); // In-place URL decode
|
||||||
|
|
||||||
|
// GET/POST parsing
|
||||||
|
bool WebReadRequest(WiFiClient *client, char *reqBuff, int reqBuffLen, char **urlStr, char **paramStr);
|
||||||
|
bool ParseParam(char **paramStr, char **name, char **value); // Get next name/parameter from a param string
|
||||||
|
bool IsIndexHTML(const char *url); // Is this meant to be index.html (/, index.htm, etc.)
|
||||||
|
|
||||||
|
// HTML FORM generation
|
||||||
|
void WebFormText(WiFiClient *client, /*const char **/ PGM_P label, const char *name, const char *value, bool enabled);
|
||||||
|
void WebFormText(WiFiClient *client, /*const char **/ PGM_P label, const char *name, const int value, bool enabled);
|
||||||
|
void WebFormCheckbox(WiFiClient *client, /*const char **/ PGM_P label, const char *name, bool checked, bool enabled);
|
||||||
|
void WebFormCheckboxDisabler(WiFiClient *client, PGM_P /*const char **/label, const char *name, bool invert, bool checked, bool enabled, const char *ids[]);
|
||||||
|
|
||||||
|
// HTML FORM parsing
|
||||||
|
int ParseInt(char *src, int *dest);
|
||||||
|
void Read4Int(char *str, byte *p);
|
||||||
|
#define ParamText(name, dest) { if (!strcmp(namePtr, (name))) strlcpy((dest), valPtr, sizeof(dest)); }
|
||||||
|
#define ParamCheckbox(name, dest) { if (!strcmp(namePtr, (name))) (dest) = !strcmp("on", valPtr); }
|
||||||
|
#define ParamInt(name, dest) { if (!strcmp(namePtr, (name))) ParseInt(valPtr, &dest); }
|
||||||
|
#define Param4Int(name, dest) { if (!strcmp(namePtr, (name))) Read4Int(valPtr, (dest)); }
|
||||||
|
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
AudioFileSource KEYWORD1
|
||||||
|
AudioFileSourceSPIFFS KEYWORD1
|
||||||
|
AudioFileSourceLittleFS KEYWORD1
|
||||||
|
AudioFileSourceFS KEYWORD1
|
||||||
|
AudioFileSourcePROGMEM KEYWORD1
|
||||||
|
AudioFileSourceHTTPStream KEYWORD1
|
||||||
|
AudioFileSourceICYStream KEYWORD1
|
||||||
|
AudioFileSourceID3 KEYWORD1
|
||||||
|
AudioFileSourceSD KEYWORD1
|
||||||
|
AudioFileSourceBuffer KEYWORD1
|
||||||
|
AudioFileSourceSPIRAMBuffer KEYWORD1
|
||||||
|
AudioGenerator KEYWORD1
|
||||||
|
AudioGeneratorAAC KEYWORD1
|
||||||
|
AudioGeneratorFLAC KEYWORD1
|
||||||
|
AudioGeneratorMOD KEYWORD1
|
||||||
|
AudioGeneratorMIDI KEYWORD1
|
||||||
|
AudioGeneratorMP3 KEYWORD1
|
||||||
|
AudioGeneratorOpus KEYWORD1
|
||||||
|
AudioGeneratorRTTTL KEYWORD1
|
||||||
|
AudioGeneratorTalkie KEYWORD1
|
||||||
|
AudioGeneratorWAV KEYWORD1
|
||||||
|
AudioOutput KEYWORD1
|
||||||
|
AudioOutputI2S KEYWORD1
|
||||||
|
AudioOutputI2SNoDAC KEYWORD1
|
||||||
|
AudioOutputNull KEYWORD1
|
||||||
|
AudioOutputBuffer KEYWORD1
|
||||||
|
AudioOutputSerialWAV KEYWORD1
|
||||||
|
AudioOutputSPIFFSWAV KEYWORD1
|
||||||
|
AudioOutputMixer KEYWORD1
|
||||||
|
AudioOutputMixerStub KEYWORD1
|
||||||
|
AudioOutputSPDIF KEYWORD1
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"name": "ESP8266Audio",
|
||||||
|
"description": "Audio file format and I2S DAC library",
|
||||||
|
"keywords": "ESP8266, ESP32, MP3, AAC, WAV, MOD, FLAC, RTTTL, MIDI, I2S, DAC, Delta-Sigma, TTS",
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Earle F. Philhower, III",
|
||||||
|
"email": "earlephilhower@yahoo.com",
|
||||||
|
"url": "https://github.com/earlephilhower/ESP8266Audio",
|
||||||
|
"maintainer": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/earlephilhower/ESP8266Audio"
|
||||||
|
},
|
||||||
|
"version": "1.5.0",
|
||||||
|
"homepage": "https://github.com/earlephilhower/ESP8266Audio",
|
||||||
|
"dependencies": {
|
||||||
|
"SPI": "1.0"
|
||||||
|
},
|
||||||
|
"frameworks": "Arduino",
|
||||||
|
"examples": [
|
||||||
|
"examples/*/*.ino"
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
name=ESP8266Audio
|
||||||
|
version=1.5.0
|
||||||
|
author=Earle F. Philhower, III
|
||||||
|
maintainer=Earle F. Philhower, III
|
||||||
|
sentence=Audio file and I2S sound playing routines.
|
||||||
|
paragraph=Decode compressed MP3, AAC, FLAC, Screamtracker MOD, MIDI, RTTL, TI Talkie, and WAV and play on an I2S DAC or a software-driven delta-sigma DAC and 1-transistor amplifier.
|
||||||
|
category=Signal Input/Output
|
||||||
|
url=https://github.com/earlephilhower/ESP8266Audio
|
||||||
|
architectures=esp8266,esp32
|
|
@ -0,0 +1,51 @@
|
||||||
|
/*
|
||||||
|
AudioFileSource
|
||||||
|
Base class of an input "file" to be used by AudioGenerator
|
||||||
|
|
||||||
|
Copyright (C) 2017 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _AUDIOFILESOURCE_H
|
||||||
|
#define _AUDIOFILESOURCE_H
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include "AudioStatus.h"
|
||||||
|
|
||||||
|
class AudioFileSource
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
AudioFileSource() {};
|
||||||
|
virtual ~AudioFileSource() {};
|
||||||
|
virtual bool open(const char *filename) { (void)filename; return false; };
|
||||||
|
virtual uint32_t read(void *data, uint32_t len) { (void)data; (void)len; return 0; };
|
||||||
|
virtual uint32_t readNonBlock(void *data, uint32_t len) { return read(data, len); };
|
||||||
|
virtual bool seek(int32_t pos, int dir) { (void)pos; (void)dir; return false; };
|
||||||
|
virtual bool close() { return false; };
|
||||||
|
virtual bool isOpen() { return false; };
|
||||||
|
virtual uint32_t getSize() { return 0; };
|
||||||
|
virtual uint32_t getPos() { return 0; };
|
||||||
|
virtual bool loop() { return true; };
|
||||||
|
|
||||||
|
public:
|
||||||
|
virtual bool RegisterMetadataCB(AudioStatus::metadataCBFn fn, void *data) { return cb.RegisterMetadataCB(fn, data); }
|
||||||
|
virtual bool RegisterStatusCB(AudioStatus::statusCBFn fn, void *data) { return cb.RegisterStatusCB(fn, data); }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
AudioStatus cb;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
|
@ -0,0 +1,190 @@
|
||||||
|
/*
|
||||||
|
AudioFileSourceBuffer
|
||||||
|
Double-buffered file source using system RAM
|
||||||
|
|
||||||
|
Copyright (C) 2017 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include "AudioFileSourceBuffer.h"
|
||||||
|
|
||||||
|
#pragma GCC optimize ("O3")
|
||||||
|
|
||||||
|
AudioFileSourceBuffer::AudioFileSourceBuffer(AudioFileSource *source, uint32_t buffSizeBytes)
|
||||||
|
{
|
||||||
|
buffSize = buffSizeBytes;
|
||||||
|
buffer = (uint8_t*)malloc(sizeof(uint8_t) * buffSize);
|
||||||
|
if (!buffer) audioLogger->printf_P(PSTR("Unable to allocate AudioFileSourceBuffer::buffer[]\n"));
|
||||||
|
deallocateBuffer = true;
|
||||||
|
writePtr = 0;
|
||||||
|
readPtr = 0;
|
||||||
|
src = source;
|
||||||
|
length = 0;
|
||||||
|
filled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioFileSourceBuffer::AudioFileSourceBuffer(AudioFileSource *source, void *inBuff, uint32_t buffSizeBytes)
|
||||||
|
{
|
||||||
|
buffSize = buffSizeBytes;
|
||||||
|
buffer = (uint8_t*)inBuff;
|
||||||
|
deallocateBuffer = false;
|
||||||
|
writePtr = 0;
|
||||||
|
readPtr = 0;
|
||||||
|
src = source;
|
||||||
|
length = 0;
|
||||||
|
filled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioFileSourceBuffer::~AudioFileSourceBuffer()
|
||||||
|
{
|
||||||
|
if (deallocateBuffer) free(buffer);
|
||||||
|
buffer = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioFileSourceBuffer::seek(int32_t pos, int dir)
|
||||||
|
{
|
||||||
|
if(dir == SEEK_CUR && (readPtr+pos) < length) {
|
||||||
|
readPtr += pos;
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
// Invalidate
|
||||||
|
readPtr = 0;
|
||||||
|
writePtr = 0;
|
||||||
|
length = 0;
|
||||||
|
return src->seek(pos, dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioFileSourceBuffer::close()
|
||||||
|
{
|
||||||
|
if (deallocateBuffer) free(buffer);
|
||||||
|
buffer = NULL;
|
||||||
|
return src->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioFileSourceBuffer::isOpen()
|
||||||
|
{
|
||||||
|
return src->isOpen();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t AudioFileSourceBuffer::getSize()
|
||||||
|
{
|
||||||
|
return src->getSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t AudioFileSourceBuffer::getPos()
|
||||||
|
{
|
||||||
|
return src->getPos();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t AudioFileSourceBuffer::getFillLevel()
|
||||||
|
{
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t AudioFileSourceBuffer::read(void *data, uint32_t len)
|
||||||
|
{
|
||||||
|
if (!buffer) return src->read(data, len);
|
||||||
|
|
||||||
|
uint32_t bytes = 0;
|
||||||
|
if (!filled) {
|
||||||
|
// Fill up completely before returning any data at all
|
||||||
|
cb.st(STATUS_FILLING, PSTR("Refilling buffer"));
|
||||||
|
length = src->read(buffer, buffSize);
|
||||||
|
writePtr = length % buffSize;
|
||||||
|
filled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pull from buffer until we've got none left or we've satisfied the request
|
||||||
|
uint8_t *ptr = reinterpret_cast<uint8_t*>(data);
|
||||||
|
uint32_t toReadFromBuffer = (len < length) ? len : length;
|
||||||
|
if ( (toReadFromBuffer > 0) && (readPtr >= writePtr) ) {
|
||||||
|
uint32_t toReadToEnd = (toReadFromBuffer < (uint32_t)(buffSize - readPtr)) ? toReadFromBuffer : (buffSize - readPtr);
|
||||||
|
memcpy(ptr, &buffer[readPtr], toReadToEnd);
|
||||||
|
readPtr = (readPtr + toReadToEnd) % buffSize;
|
||||||
|
len -= toReadToEnd;
|
||||||
|
length -= toReadToEnd;
|
||||||
|
ptr += toReadToEnd;
|
||||||
|
bytes += toReadToEnd;
|
||||||
|
toReadFromBuffer -= toReadToEnd;
|
||||||
|
}
|
||||||
|
if (toReadFromBuffer > 0) { // We know RP < WP at this point
|
||||||
|
memcpy(ptr, &buffer[readPtr], toReadFromBuffer);
|
||||||
|
readPtr = (readPtr + toReadFromBuffer) % buffSize;
|
||||||
|
len -= toReadFromBuffer;
|
||||||
|
length -= toReadFromBuffer;
|
||||||
|
ptr += toReadFromBuffer;
|
||||||
|
bytes += toReadFromBuffer;
|
||||||
|
toReadFromBuffer -= toReadFromBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len) {
|
||||||
|
// Still need more, try direct read from src
|
||||||
|
bytes += src->read(ptr, len);
|
||||||
|
// We're out of buffered data, need to force a complete refill. Thanks, @armSeb
|
||||||
|
readPtr = 0;
|
||||||
|
writePtr = 0;
|
||||||
|
length = 0;
|
||||||
|
filled = false;
|
||||||
|
cb.st(STATUS_UNDERFLOW, PSTR("Buffer underflow"));
|
||||||
|
}
|
||||||
|
|
||||||
|
fill();
|
||||||
|
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioFileSourceBuffer::fill()
|
||||||
|
{
|
||||||
|
if (!buffer) return;
|
||||||
|
|
||||||
|
if (length < buffSize) {
|
||||||
|
// Now try and opportunistically fill the buffer
|
||||||
|
if (readPtr > writePtr) {
|
||||||
|
if (readPtr == writePtr+1) return;
|
||||||
|
uint32_t bytesAvailMid = readPtr - writePtr - 1;
|
||||||
|
int cnt = src->readNonBlock(&buffer[writePtr], bytesAvailMid);
|
||||||
|
length += cnt;
|
||||||
|
writePtr = (writePtr + cnt) % buffSize;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buffSize > writePtr) {
|
||||||
|
uint32_t bytesAvailEnd = buffSize - writePtr;
|
||||||
|
int cnt = src->readNonBlock(&buffer[writePtr], bytesAvailEnd);
|
||||||
|
length += cnt;
|
||||||
|
writePtr = (writePtr + cnt) % buffSize;
|
||||||
|
if (cnt != (int)bytesAvailEnd) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (readPtr > 1) {
|
||||||
|
uint32_t bytesAvailStart = readPtr - 1;
|
||||||
|
int cnt = src->readNonBlock(&buffer[writePtr], bytesAvailStart);
|
||||||
|
length += cnt;
|
||||||
|
writePtr = (writePtr + cnt) % buffSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
bool AudioFileSourceBuffer::loop()
|
||||||
|
{
|
||||||
|
if (!src->loop()) return false;
|
||||||
|
fill();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
/*
|
||||||
|
AudioFileSourceBuffer
|
||||||
|
Double-buffered input file using system RAM
|
||||||
|
|
||||||
|
Copyright (C) 2017 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _AUDIOFILESOURCEBUFFER_H
|
||||||
|
#define _AUDIOFILESOURCEBUFFER_H
|
||||||
|
|
||||||
|
#include "AudioFileSource.h"
|
||||||
|
|
||||||
|
|
||||||
|
class AudioFileSourceBuffer : public AudioFileSource
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
AudioFileSourceBuffer(AudioFileSource *in, uint32_t bufferBytes);
|
||||||
|
AudioFileSourceBuffer(AudioFileSource *in, void *buffer, uint32_t bufferBytes); // Pre-allocated buffer by app
|
||||||
|
virtual ~AudioFileSourceBuffer() override;
|
||||||
|
|
||||||
|
virtual uint32_t read(void *data, uint32_t len) override;
|
||||||
|
virtual bool seek(int32_t pos, int dir) override;
|
||||||
|
virtual bool close() override;
|
||||||
|
virtual bool isOpen() override;
|
||||||
|
virtual uint32_t getSize() override;
|
||||||
|
virtual uint32_t getPos() override;
|
||||||
|
virtual bool loop() override;
|
||||||
|
|
||||||
|
virtual uint32_t getFillLevel();
|
||||||
|
|
||||||
|
enum { STATUS_FILLING=2, STATUS_UNDERFLOW };
|
||||||
|
|
||||||
|
private:
|
||||||
|
virtual void fill();
|
||||||
|
|
||||||
|
private:
|
||||||
|
AudioFileSource *src;
|
||||||
|
uint32_t buffSize;
|
||||||
|
uint8_t *buffer;
|
||||||
|
bool deallocateBuffer;
|
||||||
|
uint32_t writePtr;
|
||||||
|
uint32_t readPtr;
|
||||||
|
uint32_t length;
|
||||||
|
bool filled;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
/*
|
||||||
|
AudioFileSourceFS
|
||||||
|
Input Arduion "file" to be used by AudioGenerator
|
||||||
|
|
||||||
|
Copyright (C) 2017 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _AUDIOFILESOURCEFATFS_H
|
||||||
|
#define _AUDIOFILESOURCEFATFS_H
|
||||||
|
|
||||||
|
#ifdef ESP32
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <FS.h>
|
||||||
|
#include <FFat.h>
|
||||||
|
|
||||||
|
#include "AudioFileSource.h"
|
||||||
|
#include "AudioFileSourceFS.h"
|
||||||
|
|
||||||
|
/*
|
||||||
|
AudioFileSource for FAT filesystem.
|
||||||
|
*/
|
||||||
|
class AudioFileSourceFATFS : public AudioFileSourceFS
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
AudioFileSourceFATFS() : AudioFileSourceFS(FFat) {};
|
||||||
|
AudioFileSourceFATFS(const char *filename) : AudioFileSourceFS(FFat) {
|
||||||
|
// We call open() ourselves because calling AudioFileSourceFS(FFat, filename)
|
||||||
|
// would call the parent open() and we do not want that
|
||||||
|
open(filename);
|
||||||
|
};
|
||||||
|
|
||||||
|
virtual bool open(const char *filename) override {
|
||||||
|
// make sure that the FATFS filesystem has been mounted
|
||||||
|
if (!FFat.begin()) {
|
||||||
|
audioLogger->printf_P(PSTR("Unable to initialize FATFS filesystem\n"));
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
// now that the fielsystem has been mounted, we can call the regular parent open() function
|
||||||
|
return AudioFileSourceFS::open(filename);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Others are inherited from base
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
/*
|
||||||
|
AudioFileSourceFS
|
||||||
|
Input "file" to be used by AudioGenerator
|
||||||
|
|
||||||
|
Copyright (C) 2017 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "AudioFileSourceFS.h"
|
||||||
|
#ifdef ESP32
|
||||||
|
#include "SPIFFS.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
AudioFileSourceFS::AudioFileSourceFS(FS &fs, const char *filename)
|
||||||
|
{
|
||||||
|
filesystem = &fs;
|
||||||
|
open(filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioFileSourceFS::open(const char *filename)
|
||||||
|
{
|
||||||
|
#ifndef ESP32
|
||||||
|
filesystem->begin();
|
||||||
|
#endif
|
||||||
|
f = filesystem->open(filename, "r");
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioFileSourceFS::~AudioFileSourceFS()
|
||||||
|
{
|
||||||
|
if (f) f.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t AudioFileSourceFS::read(void *data, uint32_t len)
|
||||||
|
{
|
||||||
|
return f.read(reinterpret_cast<uint8_t*>(data), len);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioFileSourceFS::seek(int32_t pos, int dir)
|
||||||
|
{
|
||||||
|
return f.seek(pos, (dir==SEEK_SET)?SeekSet:(dir==SEEK_CUR)?SeekCur:SeekEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioFileSourceFS::close()
|
||||||
|
{
|
||||||
|
f.close();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioFileSourceFS::isOpen()
|
||||||
|
{
|
||||||
|
return f?true:false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t AudioFileSourceFS::getSize()
|
||||||
|
{
|
||||||
|
if (!f) return 0;
|
||||||
|
return f.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
/*
|
||||||
|
AudioFileSourceFS
|
||||||
|
Input Arduion "file" to be used by AudioGenerator
|
||||||
|
|
||||||
|
Copyright (C) 2017 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _AUDIOFILESOURCEFS_H
|
||||||
|
#define _AUDIOFILESOURCEFS_H
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <FS.h>
|
||||||
|
|
||||||
|
#include "AudioFileSource.h"
|
||||||
|
|
||||||
|
class AudioFileSourceFS : public AudioFileSource
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
AudioFileSourceFS(FS &fs) { filesystem = &fs; }
|
||||||
|
AudioFileSourceFS(FS &fs, const char *filename);
|
||||||
|
virtual ~AudioFileSourceFS() override;
|
||||||
|
|
||||||
|
virtual bool open(const char *filename) override;
|
||||||
|
virtual uint32_t read(void *data, uint32_t len) override;
|
||||||
|
virtual bool seek(int32_t pos, int dir) override;
|
||||||
|
virtual bool close() override;
|
||||||
|
virtual bool isOpen() override;
|
||||||
|
virtual uint32_t getSize() override;
|
||||||
|
virtual uint32_t getPos() override { if (!f) return 0; else return f.position(); };
|
||||||
|
|
||||||
|
private:
|
||||||
|
FS *filesystem;
|
||||||
|
File f;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
|
@ -0,0 +1,154 @@
|
||||||
|
/*
|
||||||
|
AudioFileSourceHTTPStream
|
||||||
|
Streaming HTTP source
|
||||||
|
|
||||||
|
Copyright (C) 2017 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "AudioFileSourceHTTPStream.h"
|
||||||
|
|
||||||
|
AudioFileSourceHTTPStream::AudioFileSourceHTTPStream()
|
||||||
|
{
|
||||||
|
pos = 0;
|
||||||
|
reconnectTries = 0;
|
||||||
|
saveURL[0] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioFileSourceHTTPStream::AudioFileSourceHTTPStream(const char *url)
|
||||||
|
{
|
||||||
|
saveURL[0] = 0;
|
||||||
|
reconnectTries = 0;
|
||||||
|
open(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioFileSourceHTTPStream::open(const char *url)
|
||||||
|
{
|
||||||
|
pos = 0;
|
||||||
|
http.begin(client, url);
|
||||||
|
http.setReuse(true);
|
||||||
|
#ifndef ESP32
|
||||||
|
http.setFollowRedirects(true);
|
||||||
|
#endif
|
||||||
|
int code = http.GET();
|
||||||
|
if (code != HTTP_CODE_OK) {
|
||||||
|
http.end();
|
||||||
|
cb.st(STATUS_HTTPFAIL, PSTR("Can't open HTTP request"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
size = http.getSize();
|
||||||
|
strncpy(saveURL, url, sizeof(saveURL));
|
||||||
|
saveURL[sizeof(saveURL)-1] = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioFileSourceHTTPStream::~AudioFileSourceHTTPStream()
|
||||||
|
{
|
||||||
|
http.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t AudioFileSourceHTTPStream::read(void *data, uint32_t len)
|
||||||
|
{
|
||||||
|
if (data==NULL) {
|
||||||
|
audioLogger->printf_P(PSTR("ERROR! AudioFileSourceHTTPStream::read passed NULL data\n"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return readInternal(data, len, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t AudioFileSourceHTTPStream::readNonBlock(void *data, uint32_t len)
|
||||||
|
{
|
||||||
|
if (data==NULL) {
|
||||||
|
audioLogger->printf_P(PSTR("ERROR! AudioFileSourceHTTPStream::readNonBlock passed NULL data\n"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return readInternal(data, len, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t AudioFileSourceHTTPStream::readInternal(void *data, uint32_t len, bool nonBlock)
|
||||||
|
{
|
||||||
|
retry:
|
||||||
|
if (!http.connected()) {
|
||||||
|
cb.st(STATUS_DISCONNECTED, PSTR("Stream disconnected"));
|
||||||
|
http.end();
|
||||||
|
for (int i = 0; i < reconnectTries; i++) {
|
||||||
|
char buff[32];
|
||||||
|
sprintf_P(buff, PSTR("Attempting to reconnect, try %d"), i);
|
||||||
|
cb.st(STATUS_RECONNECTING, buff);
|
||||||
|
delay(reconnectDelayMs);
|
||||||
|
if (open(saveURL)) {
|
||||||
|
cb.st(STATUS_RECONNECTED, PSTR("Stream reconnected"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!http.connected()) {
|
||||||
|
cb.st(STATUS_DISCONNECTED, PSTR("Unable to reconnect"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((size > 0) && (pos >= size)) return 0;
|
||||||
|
|
||||||
|
WiFiClient *stream = http.getStreamPtr();
|
||||||
|
|
||||||
|
// Can't read past EOF...
|
||||||
|
if ( (size > 0) && (len > (uint32_t)(pos - size)) ) len = pos - size;
|
||||||
|
|
||||||
|
if (!nonBlock) {
|
||||||
|
int start = millis();
|
||||||
|
while ((stream->available() < (int)len) && (millis() - start < 500)) yield();
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t avail = stream->available();
|
||||||
|
if (!nonBlock && !avail) {
|
||||||
|
cb.st(STATUS_NODATA, PSTR("No stream data available"));
|
||||||
|
http.end();
|
||||||
|
goto retry;
|
||||||
|
}
|
||||||
|
if (avail == 0) return 0;
|
||||||
|
if (avail < len) len = avail;
|
||||||
|
|
||||||
|
int read = stream->read(reinterpret_cast<uint8_t*>(data), len);
|
||||||
|
pos += read;
|
||||||
|
return read;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioFileSourceHTTPStream::seek(int32_t pos, int dir)
|
||||||
|
{
|
||||||
|
audioLogger->printf_P(PSTR("ERROR! AudioFileSourceHTTPStream::seek not implemented!"));
|
||||||
|
(void) pos;
|
||||||
|
(void) dir;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioFileSourceHTTPStream::close()
|
||||||
|
{
|
||||||
|
http.end();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioFileSourceHTTPStream::isOpen()
|
||||||
|
{
|
||||||
|
return http.connected();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t AudioFileSourceHTTPStream::getSize()
|
||||||
|
{
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t AudioFileSourceHTTPStream::getPos()
|
||||||
|
{
|
||||||
|
return pos;
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
/*
|
||||||
|
AudioFileSourceHTTPStream
|
||||||
|
Connect to a HTTP based streaming service
|
||||||
|
|
||||||
|
Copyright (C) 2017 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _AUDIOFILESOURCEHTTPSTREAM_H
|
||||||
|
#define _AUDIOFILESOURCEHTTPSTREAM_H
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#ifdef ESP32
|
||||||
|
#include <HTTPClient.h>
|
||||||
|
#else
|
||||||
|
#include <ESP8266HTTPClient.h>
|
||||||
|
#endif
|
||||||
|
#include "AudioFileSource.h"
|
||||||
|
|
||||||
|
class AudioFileSourceHTTPStream : public AudioFileSource
|
||||||
|
{
|
||||||
|
friend class AudioFileSourceICYStream;
|
||||||
|
|
||||||
|
public:
|
||||||
|
AudioFileSourceHTTPStream();
|
||||||
|
AudioFileSourceHTTPStream(const char *url);
|
||||||
|
virtual ~AudioFileSourceHTTPStream() override;
|
||||||
|
|
||||||
|
virtual bool open(const char *url) override;
|
||||||
|
virtual uint32_t read(void *data, uint32_t len) override;
|
||||||
|
virtual uint32_t readNonBlock(void *data, uint32_t len) override;
|
||||||
|
virtual bool seek(int32_t pos, int dir) override;
|
||||||
|
virtual bool close() override;
|
||||||
|
virtual bool isOpen() override;
|
||||||
|
virtual uint32_t getSize() override;
|
||||||
|
virtual uint32_t getPos() override;
|
||||||
|
bool SetReconnect(int tries, int delayms) { reconnectTries = tries; reconnectDelayMs = delayms; return true; }
|
||||||
|
|
||||||
|
enum { STATUS_HTTPFAIL=2, STATUS_DISCONNECTED, STATUS_RECONNECTING, STATUS_RECONNECTED, STATUS_NODATA };
|
||||||
|
|
||||||
|
private:
|
||||||
|
virtual uint32_t readInternal(void *data, uint32_t len, bool nonBlock);
|
||||||
|
WiFiClient client;
|
||||||
|
HTTPClient http;
|
||||||
|
int pos;
|
||||||
|
int size;
|
||||||
|
int reconnectTries;
|
||||||
|
int reconnectDelayMs;
|
||||||
|
char saveURL[128];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
|
@ -0,0 +1,213 @@
|
||||||
|
/*
|
||||||
|
AudioFileSourceICYStream
|
||||||
|
Streaming Shoutcast ICY source
|
||||||
|
|
||||||
|
Copyright (C) 2017 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
#define _GNU_SOURCE
|
||||||
|
|
||||||
|
#include "AudioFileSourceICYStream.h"
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
AudioFileSourceICYStream::AudioFileSourceICYStream()
|
||||||
|
{
|
||||||
|
pos = 0;
|
||||||
|
reconnectTries = 0;
|
||||||
|
saveURL[0] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioFileSourceICYStream::AudioFileSourceICYStream(const char *url)
|
||||||
|
{
|
||||||
|
saveURL[0] = 0;
|
||||||
|
reconnectTries = 0;
|
||||||
|
open(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioFileSourceICYStream::open(const char *url)
|
||||||
|
{
|
||||||
|
static const char *hdr[] = { "icy-metaint", "icy-name", "icy-genre", "icy-br" };
|
||||||
|
pos = 0;
|
||||||
|
http.begin(client, url);
|
||||||
|
http.addHeader("Icy-MetaData", "1");
|
||||||
|
http.collectHeaders( hdr, 4 );
|
||||||
|
http.setReuse(true);
|
||||||
|
int code = http.GET();
|
||||||
|
if (code != HTTP_CODE_OK) {
|
||||||
|
http.end();
|
||||||
|
cb.st(STATUS_HTTPFAIL, PSTR("Can't open HTTP request"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (http.hasHeader(hdr[0])) {
|
||||||
|
String ret = http.header(hdr[0]);
|
||||||
|
icyMetaInt = ret.toInt();
|
||||||
|
} else {
|
||||||
|
icyMetaInt = 0;
|
||||||
|
}
|
||||||
|
if (http.hasHeader(hdr[1])) {
|
||||||
|
String ret = http.header(hdr[1]);
|
||||||
|
// cb.md("SiteName", false, ret.c_str());
|
||||||
|
}
|
||||||
|
if (http.hasHeader(hdr[2])) {
|
||||||
|
String ret = http.header(hdr[2]);
|
||||||
|
// cb.md("Genre", false, ret.c_str());
|
||||||
|
}
|
||||||
|
if (http.hasHeader(hdr[3])) {
|
||||||
|
String ret = http.header(hdr[3]);
|
||||||
|
// cb.md("Bitrate", false, ret.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
icyByteCount = 0;
|
||||||
|
size = http.getSize();
|
||||||
|
strncpy(saveURL, url, sizeof(saveURL));
|
||||||
|
saveURL[sizeof(saveURL)-1] = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioFileSourceICYStream::~AudioFileSourceICYStream()
|
||||||
|
{
|
||||||
|
http.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t AudioFileSourceICYStream::readInternal(void *data, uint32_t len, bool nonBlock)
|
||||||
|
{
|
||||||
|
retry:
|
||||||
|
if (!http.connected()) {
|
||||||
|
cb.st(STATUS_DISCONNECTED, PSTR("Stream disconnected"));
|
||||||
|
http.end();
|
||||||
|
for (int i = 0; i < reconnectTries; i++) {
|
||||||
|
char buff[32];
|
||||||
|
sprintf_P(buff, PSTR("Attempting to reconnect, try %d"), i);
|
||||||
|
cb.st(STATUS_RECONNECTING, buff);
|
||||||
|
delay(reconnectDelayMs);
|
||||||
|
if (open(saveURL)) {
|
||||||
|
cb.st(STATUS_RECONNECTED, PSTR("Stream reconnected"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!http.connected()) {
|
||||||
|
cb.st(STATUS_DISCONNECTED, PSTR("Unable to reconnect"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((size > 0) && (pos >= size)) return 0;
|
||||||
|
|
||||||
|
WiFiClient *stream = http.getStreamPtr();
|
||||||
|
|
||||||
|
// Can't read past EOF...
|
||||||
|
if ( (size > 0) && (len > (uint32_t)(pos - size)) ) len = pos - size;
|
||||||
|
|
||||||
|
if (!nonBlock) {
|
||||||
|
int start = millis();
|
||||||
|
while ((stream->available() < (int)len) && (millis() - start < 500)) yield();
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t avail = stream->available();
|
||||||
|
if (!nonBlock && !avail) {
|
||||||
|
cb.st(STATUS_NODATA, PSTR("No stream data available"));
|
||||||
|
http.end();
|
||||||
|
goto retry;
|
||||||
|
}
|
||||||
|
if (avail == 0) return 0;
|
||||||
|
if (avail < len) len = avail;
|
||||||
|
|
||||||
|
int read = 0;
|
||||||
|
int ret = 0;
|
||||||
|
// If the read would hit an ICY block, split it up...
|
||||||
|
if (((int)(icyByteCount + len) > (int)icyMetaInt) && (icyMetaInt > 0)) {
|
||||||
|
int beforeIcy = icyMetaInt - icyByteCount;
|
||||||
|
if (beforeIcy > 0) {
|
||||||
|
ret = stream->read(reinterpret_cast<uint8_t*>(data), beforeIcy);
|
||||||
|
if (ret < 0) ret = 0;
|
||||||
|
read += ret;
|
||||||
|
pos += ret;
|
||||||
|
len -= ret;
|
||||||
|
data = (void *)(reinterpret_cast<char*>(data) + ret);
|
||||||
|
icyByteCount += ret;
|
||||||
|
if (ret != beforeIcy) return read; // Partial read
|
||||||
|
}
|
||||||
|
|
||||||
|
// ICY MD handling
|
||||||
|
int mdSize;
|
||||||
|
uint8_t c;
|
||||||
|
int mdret = stream->read(&c, 1);
|
||||||
|
if (mdret==0) return read;
|
||||||
|
mdSize = c * 16;
|
||||||
|
if ((mdret == 1) && (mdSize > 0)) {
|
||||||
|
// This is going to get ugly fast.
|
||||||
|
char icyBuff[256 + 16 + 1];
|
||||||
|
char *readInto = icyBuff + 16;
|
||||||
|
memset(icyBuff, 0, 16); // Ensure no residual matches occur
|
||||||
|
while (mdSize) {
|
||||||
|
int toRead = mdSize > 256 ? 256 : mdSize;
|
||||||
|
int ret = stream->read((uint8_t*)readInto, toRead);
|
||||||
|
if (ret < 0) return read;
|
||||||
|
if (ret == 0) { delay(1); continue; }
|
||||||
|
mdSize -= ret;
|
||||||
|
// At this point we have 0...15 = last 15 chars read from prior read plus new data
|
||||||
|
int end = 16 + ret; // The last byte of valid data
|
||||||
|
char *header = (char *)memmem((void*)icyBuff, end, (void*)"StreamTitle=", 12);
|
||||||
|
if (!header) {
|
||||||
|
// No match, so move the last 16 bytes back to the start and continue
|
||||||
|
memmove(icyBuff, icyBuff+end-16, 16);
|
||||||
|
delay(1);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Found header, now move it to the front
|
||||||
|
int lastValidByte = end - (header -icyBuff) + 1;
|
||||||
|
memmove(icyBuff, header, lastValidByte);
|
||||||
|
// Now fill the buffer to the end with read data
|
||||||
|
while (mdSize && lastValidByte < 255) {
|
||||||
|
int toRead = mdSize > (256 - lastValidByte) ? (256 - lastValidByte) : mdSize;
|
||||||
|
ret = stream->read((uint8_t*)icyBuff + lastValidByte, toRead);
|
||||||
|
if (ret==-1) return read; // error
|
||||||
|
if (ret == 0) { delay(1); continue; }
|
||||||
|
mdSize -= ret;
|
||||||
|
lastValidByte += ret;
|
||||||
|
}
|
||||||
|
// Buffer now contains StreamTitle=....., parse it
|
||||||
|
char *p = icyBuff+12;
|
||||||
|
if (*p=='\'' || *p== '"' ) {
|
||||||
|
char closing[] = { *p, ';', '\0' };
|
||||||
|
char *psz = strstr( p+1, closing );
|
||||||
|
if( !psz ) psz = strchr( &icyBuff[13], ';' );
|
||||||
|
if( psz ) *psz = '\0';
|
||||||
|
p++;
|
||||||
|
} else {
|
||||||
|
char *psz = strchr( p, ';' );
|
||||||
|
if( psz ) *psz = '\0';
|
||||||
|
}
|
||||||
|
cb.md("StreamTitle", false, p);
|
||||||
|
|
||||||
|
// Now skip rest of MD block
|
||||||
|
while (mdSize) {
|
||||||
|
int toRead = mdSize > 256 ? 256 : mdSize;
|
||||||
|
ret = stream->read((uint8_t*)icyBuff, toRead);
|
||||||
|
if (ret < 0) return read;
|
||||||
|
if (ret == 0) { delay(1); continue; }
|
||||||
|
mdSize -= ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
icyByteCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = stream->read(reinterpret_cast<uint8_t*>(data), len);
|
||||||
|
if (ret < 0) ret = 0;
|
||||||
|
read += ret;
|
||||||
|
pos += ret;
|
||||||
|
icyByteCount += ret;
|
||||||
|
return read;
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
AudioFileSourceHTTPStream
|
||||||
|
Connect to a HTTP based streaming service
|
||||||
|
|
||||||
|
Copyright (C) 2017 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _AUDIOFILESOURCEICYSTREAM_H
|
||||||
|
#define _AUDIOFILESOURCEICYSTREAM_H
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#ifdef ESP32
|
||||||
|
#include <HTTPClient.h>
|
||||||
|
#else
|
||||||
|
#include <ESP8266HTTPClient.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "AudioFileSourceHTTPStream.h"
|
||||||
|
|
||||||
|
class AudioFileSourceICYStream : public AudioFileSourceHTTPStream
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
AudioFileSourceICYStream();
|
||||||
|
AudioFileSourceICYStream(const char *url);
|
||||||
|
virtual ~AudioFileSourceICYStream() override;
|
||||||
|
|
||||||
|
virtual bool open(const char *url) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
virtual uint32_t readInternal(void *data, uint32_t len, bool nonBlock) override;
|
||||||
|
int icyMetaInt;
|
||||||
|
int icyByteCount;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
|
@ -0,0 +1,265 @@
|
||||||
|
/*
|
||||||
|
AudioFileSourceID3
|
||||||
|
ID3 filter that extracts any ID3 fields and sends to CB function
|
||||||
|
|
||||||
|
Copyright (C) 2017 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "AudioFileSourceID3.h"
|
||||||
|
|
||||||
|
// Handle unsync operation in ID3 with custom class
|
||||||
|
class AudioFileSourceUnsync : public AudioFileSource
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
AudioFileSourceUnsync(AudioFileSource *src, int len, bool unsync);
|
||||||
|
virtual ~AudioFileSourceUnsync() override;
|
||||||
|
virtual uint32_t read(void *data, uint32_t len) override;
|
||||||
|
|
||||||
|
int getByte();
|
||||||
|
bool eof();
|
||||||
|
|
||||||
|
private:
|
||||||
|
AudioFileSource *src;
|
||||||
|
int remaining;
|
||||||
|
bool unsync;
|
||||||
|
int savedByte;
|
||||||
|
};
|
||||||
|
|
||||||
|
AudioFileSourceUnsync::AudioFileSourceUnsync(AudioFileSource *src, int len, bool unsync)
|
||||||
|
{
|
||||||
|
this->src = src;
|
||||||
|
this->remaining = len;
|
||||||
|
this->unsync = unsync;
|
||||||
|
this->savedByte = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioFileSourceUnsync::~AudioFileSourceUnsync()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t AudioFileSourceUnsync::read(void *data, uint32_t len)
|
||||||
|
{
|
||||||
|
uint32_t bytes = 0;
|
||||||
|
uint8_t *ptr = reinterpret_cast<uint8_t*>(data);
|
||||||
|
|
||||||
|
// This is only used during ID3 parsing, so no need to optimize here...
|
||||||
|
while (len--) {
|
||||||
|
int b = getByte();
|
||||||
|
if (b >= 0) {
|
||||||
|
*(ptr++) = (uint8_t)b;
|
||||||
|
bytes++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
int AudioFileSourceUnsync::getByte()
|
||||||
|
{
|
||||||
|
// If we're not unsync, just read.
|
||||||
|
if (!unsync) {
|
||||||
|
uint8_t c;
|
||||||
|
if (!remaining) return -1;
|
||||||
|
remaining--;
|
||||||
|
if (1 != src->read(&c, 1)) return -1;
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we've saved a pre-read character, return it immediately
|
||||||
|
if (savedByte >= 0) {
|
||||||
|
int s = savedByte;
|
||||||
|
savedByte = -1;
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remaining <= 0) {
|
||||||
|
return -1;
|
||||||
|
} else if (remaining == 1) {
|
||||||
|
remaining--;
|
||||||
|
uint8_t c;
|
||||||
|
if (1 != src->read(&c, 1)) return -1;
|
||||||
|
else return c;
|
||||||
|
} else {
|
||||||
|
uint8_t c;
|
||||||
|
remaining--;
|
||||||
|
if (1 != src->read(&c, 1)) return -1;
|
||||||
|
if (c != 0xff) {
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
// Saw 0xff, check next byte. If 0 then eat it, OTW return the 0xff
|
||||||
|
uint8_t d;
|
||||||
|
remaining--;
|
||||||
|
if (1 != src->read(&d, 1)) return c;
|
||||||
|
if (d != 0x00) {
|
||||||
|
savedByte = d;
|
||||||
|
}
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioFileSourceUnsync::eof()
|
||||||
|
{
|
||||||
|
if (remaining<=0) return true;
|
||||||
|
else return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
AudioFileSourceID3::AudioFileSourceID3(AudioFileSource *src)
|
||||||
|
{
|
||||||
|
this->src = src;
|
||||||
|
this->checked = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioFileSourceID3::~AudioFileSourceID3()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t AudioFileSourceID3::read(void *data, uint32_t len)
|
||||||
|
{
|
||||||
|
int rev = 0;
|
||||||
|
|
||||||
|
if (checked) {
|
||||||
|
return src->read(data, len);
|
||||||
|
}
|
||||||
|
checked = true;
|
||||||
|
// <10 bytes initial read, not enough space to check header
|
||||||
|
if (len<10) return src->read(data, len);
|
||||||
|
|
||||||
|
uint8_t *buff = reinterpret_cast<uint8_t*>(data);
|
||||||
|
int ret = src->read(data, 10);
|
||||||
|
if (ret<10) return ret;
|
||||||
|
|
||||||
|
if ((buff[0]!='I') || (buff[1]!='D') || (buff[2]!='3') || (buff[3]>0x04) || (buff[3]<0x02) || (buff[4]!=0)) {
|
||||||
|
return 10 + src->read(buff+10, len-10);
|
||||||
|
}
|
||||||
|
|
||||||
|
rev = buff[3];
|
||||||
|
bool unsync = false;
|
||||||
|
bool exthdr = false;
|
||||||
|
|
||||||
|
switch(rev) {
|
||||||
|
case 2:
|
||||||
|
unsync = (buff[5] & 0x80);
|
||||||
|
exthdr = false;
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
case 4:
|
||||||
|
unsync = (buff[5] & 0x80);
|
||||||
|
exthdr = (buff[5] & 0x40);
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
|
||||||
|
int id3Size = buff[6];
|
||||||
|
id3Size = id3Size << 7;
|
||||||
|
id3Size |= buff[7];
|
||||||
|
id3Size = id3Size << 7;
|
||||||
|
id3Size |= buff[8];
|
||||||
|
id3Size = id3Size << 7;
|
||||||
|
id3Size |= buff[9];
|
||||||
|
// Every read from now may be unsync'd
|
||||||
|
AudioFileSourceUnsync id3(src, id3Size, unsync);
|
||||||
|
|
||||||
|
if (exthdr) {
|
||||||
|
int ehsz = (id3.getByte()<<24) | (id3.getByte()<<16) | (id3.getByte()<<8) | (id3.getByte());
|
||||||
|
for (int j=0; j<ehsz-4; j++) id3.getByte(); // Throw it away
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
unsigned char frameid[4];
|
||||||
|
int framesize;
|
||||||
|
bool compressed;
|
||||||
|
|
||||||
|
frameid[0] = id3.getByte();
|
||||||
|
frameid[1] = id3.getByte();
|
||||||
|
frameid[2] = id3.getByte();
|
||||||
|
if (rev==2) frameid[3] = 0;
|
||||||
|
else frameid[3] = id3.getByte();
|
||||||
|
|
||||||
|
if (frameid[0]==0 && frameid[1]==0 && frameid[2]==0 && frameid[3]==0) {
|
||||||
|
// We're in padding
|
||||||
|
while (!id3.eof()) {
|
||||||
|
id3.getByte();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (rev==2) {
|
||||||
|
framesize = (id3.getByte()<<16) | (id3.getByte()<<8) | (id3.getByte());
|
||||||
|
compressed = false;
|
||||||
|
} else {
|
||||||
|
framesize = (id3.getByte()<<24) | (id3.getByte()<<16) | (id3.getByte()<<8) | (id3.getByte());
|
||||||
|
id3.getByte(); // skip 1st flag
|
||||||
|
compressed = id3.getByte()&0x80;
|
||||||
|
}
|
||||||
|
if (compressed) {
|
||||||
|
int decompsize = (id3.getByte()<<24) | (id3.getByte()<<16) | (id3.getByte()<<8) | (id3.getByte());
|
||||||
|
// TODO - add libz decompression, for now ignore this one...
|
||||||
|
(void)decompsize;
|
||||||
|
for (int j=0; j<framesize; j++)
|
||||||
|
id3.getByte();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the value and send to callback
|
||||||
|
char value[64];
|
||||||
|
uint16_t i;
|
||||||
|
bool isUnicode = (id3.getByte()==1) ? true : false;
|
||||||
|
for (i=0; i<framesize-1; i++) {
|
||||||
|
if (i<sizeof(value)-1) value[i] = id3.getByte();
|
||||||
|
else (void)id3.getByte();
|
||||||
|
}
|
||||||
|
value[i<sizeof(value)-1?i:sizeof(value)-1] = 0; // Terminate the string...
|
||||||
|
if ( (frameid[0]=='T' && frameid[1]=='A' && frameid[2]=='L' && frameid[3] == 'B' ) ||
|
||||||
|
(frameid[0]=='T' && frameid[1]=='A' && frameid[2]=='L' && rev==2) ) {
|
||||||
|
cb.md("Album", isUnicode, value);
|
||||||
|
} else if ( (frameid[0]=='T' && frameid[1]=='I' && frameid[2]=='T' && frameid[3] == '2') ||
|
||||||
|
(frameid[0]=='T' && frameid[1]=='T' && frameid[2]=='2' && rev==2) ) {
|
||||||
|
cb.md("Title", isUnicode, value);
|
||||||
|
} else if ( (frameid[0]=='T' && frameid[1]=='P' && frameid[2]=='E' && frameid[3] == '1') ||
|
||||||
|
(frameid[0]=='T' && frameid[1]=='P' && frameid[2]=='1' && rev==2) ) {
|
||||||
|
cb.md("Performer", isUnicode, value);
|
||||||
|
} else if ( (frameid[0]=='T' && frameid[1]=='Y' && frameid[2]=='E' && frameid[3] == 'R') ||
|
||||||
|
(frameid[0]=='T' && frameid[1]=='Y' && frameid[2]=='E' && rev==2) ) {
|
||||||
|
cb.md("Year", isUnicode, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (!id3.eof());
|
||||||
|
|
||||||
|
// All ID3 processing done, return to main caller
|
||||||
|
return src->read(data, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioFileSourceID3::seek(int32_t pos, int dir)
|
||||||
|
{
|
||||||
|
return src->seek(pos, dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioFileSourceID3::close()
|
||||||
|
{
|
||||||
|
return src->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioFileSourceID3::isOpen()
|
||||||
|
{
|
||||||
|
return src->isOpen();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t AudioFileSourceID3::getSize()
|
||||||
|
{
|
||||||
|
return src->getSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t AudioFileSourceID3::getPos()
|
||||||
|
{
|
||||||
|
return src->getPos();
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
/*
|
||||||
|
AudioFileSourceID3
|
||||||
|
ID3 filter that extracts any ID3 fields and sends to CB function
|
||||||
|
|
||||||
|
Copyright (C) 2017 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _AUDIOFILESOURCEID3_H
|
||||||
|
#define _AUDIOFILESOURCEID3_H
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
#include "AudioFileSource.h"
|
||||||
|
|
||||||
|
class AudioFileSourceID3 : public AudioFileSource
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
AudioFileSourceID3(AudioFileSource *src);
|
||||||
|
virtual ~AudioFileSourceID3() override;
|
||||||
|
|
||||||
|
virtual uint32_t read(void *data, uint32_t len) override;
|
||||||
|
virtual bool seek(int32_t pos, int dir) override;
|
||||||
|
virtual bool close() override;
|
||||||
|
virtual bool isOpen() override;
|
||||||
|
virtual uint32_t getSize() override;
|
||||||
|
virtual uint32_t getPos() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
AudioFileSource *src;
|
||||||
|
bool checked;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
AudioFileSourceFS
|
||||||
|
Input Arduion "file" to be used by AudioGenerator
|
||||||
|
|
||||||
|
Copyright (C) 2017 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _AUDIOFILESOURCESPIFFS_H
|
||||||
|
#define _AUDIOFILESOURCESPIFFS_H
|
||||||
|
|
||||||
|
#ifndef ESP32 // No LittleFS there, yet
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <LittleFS.h>
|
||||||
|
|
||||||
|
#include "AudioFileSource.h"
|
||||||
|
#include "AudioFileSourceFS.h"
|
||||||
|
|
||||||
|
class AudioFileSourceLittleFS : public AudioFileSourceFS
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
AudioFileSourceLittleFS() : AudioFileSourceFS(LittleFS) { };
|
||||||
|
AudioFileSourceLittleFS(const char *filename) : AudioFileSourceFS(LittleFS, filename) {};
|
||||||
|
// Others are inherited from base
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
|
@ -0,0 +1,99 @@
|
||||||
|
/*
|
||||||
|
AudioFileSourcePROGMEM
|
||||||
|
Store a "file" as a PROGMEM array and use it as audio source data
|
||||||
|
|
||||||
|
Copyright (C) 2017 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "AudioFileSourcePROGMEM.h"
|
||||||
|
|
||||||
|
AudioFileSourcePROGMEM::AudioFileSourcePROGMEM()
|
||||||
|
{
|
||||||
|
opened = false;
|
||||||
|
progmemData = NULL;
|
||||||
|
progmemLen = 0;
|
||||||
|
filePointer = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioFileSourcePROGMEM::AudioFileSourcePROGMEM(const void *data, uint32_t len)
|
||||||
|
{
|
||||||
|
open(data, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioFileSourcePROGMEM::~AudioFileSourcePROGMEM()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioFileSourcePROGMEM::open(const void *data, uint32_t len)
|
||||||
|
{
|
||||||
|
if (!data || !len) return false;
|
||||||
|
|
||||||
|
opened = true;
|
||||||
|
progmemData = data;
|
||||||
|
progmemLen = len;
|
||||||
|
filePointer = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t AudioFileSourcePROGMEM::getSize()
|
||||||
|
{
|
||||||
|
if (!opened) return 0;
|
||||||
|
return progmemLen;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioFileSourcePROGMEM::isOpen()
|
||||||
|
{
|
||||||
|
return opened;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioFileSourcePROGMEM::close()
|
||||||
|
{
|
||||||
|
opened = false;
|
||||||
|
progmemData = NULL;
|
||||||
|
progmemLen = 0;
|
||||||
|
filePointer = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioFileSourcePROGMEM::seek(int32_t pos, int dir)
|
||||||
|
{
|
||||||
|
if (!opened) return false;
|
||||||
|
uint32_t newPtr;
|
||||||
|
switch (dir) {
|
||||||
|
case SEEK_SET: newPtr = pos; break;
|
||||||
|
case SEEK_CUR: newPtr = filePointer + pos; break;
|
||||||
|
case SEEK_END: newPtr = progmemLen - pos; break;
|
||||||
|
default: return false;
|
||||||
|
}
|
||||||
|
if (newPtr > progmemLen) return false;
|
||||||
|
filePointer = newPtr;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t AudioFileSourcePROGMEM::read(void *data, uint32_t len)
|
||||||
|
{
|
||||||
|
if (!opened) return 0;
|
||||||
|
if (filePointer >= progmemLen) return 0;
|
||||||
|
|
||||||
|
uint32_t toRead = progmemLen - filePointer;
|
||||||
|
if (toRead > len) toRead = len;
|
||||||
|
|
||||||
|
memcpy_P(data, reinterpret_cast<const uint8_t*>(progmemData)+filePointer, toRead);
|
||||||
|
filePointer += toRead;
|
||||||
|
return toRead;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
/*
|
||||||
|
AudioFileSourcePROGMEM
|
||||||
|
Store a "file" as a PROGMEM array and use it as audio source data
|
||||||
|
|
||||||
|
Copyright (C) 2017 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _AUDIOFILESOURCEPROGMEM_H
|
||||||
|
#define _AUDIOFILESOURCEPROGMEM_H
|
||||||
|
|
||||||
|
#include "AudioFileSource.h"
|
||||||
|
|
||||||
|
class AudioFileSourcePROGMEM : public AudioFileSource
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
AudioFileSourcePROGMEM();
|
||||||
|
AudioFileSourcePROGMEM(const void *data, uint32_t len);
|
||||||
|
virtual ~AudioFileSourcePROGMEM() override;
|
||||||
|
virtual uint32_t read(void *data, uint32_t len) override;
|
||||||
|
virtual bool seek(int32_t pos, int dir) override;
|
||||||
|
virtual bool close() override;
|
||||||
|
virtual bool isOpen() override;
|
||||||
|
virtual uint32_t getSize() override;
|
||||||
|
virtual uint32_t getPos() override { if (!opened) return 0; else return filePointer; };
|
||||||
|
|
||||||
|
bool open(const void *data, uint32_t len);
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool opened;
|
||||||
|
const void *progmemData;
|
||||||
|
uint32_t progmemLen;
|
||||||
|
uint32_t filePointer;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
/*
|
||||||
|
AudioFileSourceSPIFFS
|
||||||
|
Input SD card "file" to be used by AudioGenerator
|
||||||
|
|
||||||
|
Copyright (C) 2017 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "AudioFileSourceSD.h"
|
||||||
|
|
||||||
|
AudioFileSourceSD::AudioFileSourceSD()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioFileSourceSD::AudioFileSourceSD(const char *filename)
|
||||||
|
{
|
||||||
|
open(filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioFileSourceSD::open(const char *filename)
|
||||||
|
{
|
||||||
|
f = SD.open(filename, FILE_READ);
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioFileSourceSD::~AudioFileSourceSD()
|
||||||
|
{
|
||||||
|
if (f) f.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t AudioFileSourceSD::read(void *data, uint32_t len)
|
||||||
|
{
|
||||||
|
return f.read(reinterpret_cast<uint8_t*>(data), len);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioFileSourceSD::seek(int32_t pos, int dir)
|
||||||
|
{
|
||||||
|
if (!f) return false;
|
||||||
|
if (dir==SEEK_SET) return f.seek(pos);
|
||||||
|
else if (dir==SEEK_CUR) return f.seek(f.position() + pos);
|
||||||
|
else if (dir==SEEK_END) return f.seek(f.size() + pos);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioFileSourceSD::close()
|
||||||
|
{
|
||||||
|
f.close();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioFileSourceSD::isOpen()
|
||||||
|
{
|
||||||
|
return f?true:false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t AudioFileSourceSD::getSize()
|
||||||
|
{
|
||||||
|
if (!f) return 0;
|
||||||
|
return f.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t AudioFileSourceSD::getPos()
|
||||||
|
{
|
||||||
|
if (!f) return 0;
|
||||||
|
return f.position();
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
/*
|
||||||
|
AudioFileSourceSPIFFS
|
||||||
|
Input SD card "file" to be used by AudioGenerator
|
||||||
|
|
||||||
|
Copyright (C) 2017 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _AUDIOFILESOURCESD_H
|
||||||
|
#define _AUDIOFILESOURCESD_H
|
||||||
|
|
||||||
|
#include "AudioFileSource.h"
|
||||||
|
#include <SD.h>
|
||||||
|
|
||||||
|
|
||||||
|
class AudioFileSourceSD : public AudioFileSource
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
AudioFileSourceSD();
|
||||||
|
AudioFileSourceSD(const char *filename);
|
||||||
|
virtual ~AudioFileSourceSD() override;
|
||||||
|
|
||||||
|
virtual bool open(const char *filename) override;
|
||||||
|
virtual uint32_t read(void *data, uint32_t len) override;
|
||||||
|
virtual bool seek(int32_t pos, int dir) override;
|
||||||
|
virtual bool close() override;
|
||||||
|
virtual bool isOpen() override;
|
||||||
|
virtual uint32_t getSize() override;
|
||||||
|
virtual uint32_t getPos() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
File f;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
/*
|
||||||
|
AudioFileSourceFS
|
||||||
|
Input Arduion "file" to be used by AudioGenerator
|
||||||
|
|
||||||
|
Copyright (C) 2017 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _AUDIOFILESOURCESPIFFS_H
|
||||||
|
#define _AUDIOFILESOURCESPIFFS_H
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <FS.h>
|
||||||
|
|
||||||
|
#include "AudioFileSource.h"
|
||||||
|
#include "AudioFileSourceFS.h"
|
||||||
|
|
||||||
|
class AudioFileSourceSPIFFS : public AudioFileSourceFS
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
AudioFileSourceSPIFFS() : AudioFileSourceFS(SPIFFS) { };
|
||||||
|
AudioFileSourceSPIFFS(const char *filename) : AudioFileSourceFS(SPIFFS, filename) {};
|
||||||
|
// Others are inherited from base
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
|
@ -0,0 +1,167 @@
|
||||||
|
/*
|
||||||
|
AudioFileSourceSPIRAMBuffer
|
||||||
|
Buffered file source in external SPI RAM
|
||||||
|
|
||||||
|
Copyright (C) 2017 Sebastien Decourriere
|
||||||
|
Based on AudioFileSourceBuffer class from Earle F. Philhower, III
|
||||||
|
|
||||||
|
Copyright (C) 2020 Earle F. Philhower, III
|
||||||
|
Rewritten for speed and functionality
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include "AudioFileSourceSPIRAMBuffer.h"
|
||||||
|
|
||||||
|
#pragma GCC optimize ("O3")
|
||||||
|
|
||||||
|
AudioFileSourceSPIRAMBuffer::AudioFileSourceSPIRAMBuffer(AudioFileSource *source, uint8_t csPin, uint32_t buffSizeBytes)
|
||||||
|
{
|
||||||
|
ram.begin(40, csPin);
|
||||||
|
ramSize = buffSizeBytes;
|
||||||
|
writePtr = 0;
|
||||||
|
readPtr = 0;
|
||||||
|
filled = false;
|
||||||
|
src = source;
|
||||||
|
audioLogger->printf_P(PSTR("SPI RAM buffer size: %u Bytes\n"), ramSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioFileSourceSPIRAMBuffer::~AudioFileSourceSPIRAMBuffer()
|
||||||
|
{
|
||||||
|
ram.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioFileSourceSPIRAMBuffer::seek(int32_t pos, int dir)
|
||||||
|
{
|
||||||
|
// Invalidate
|
||||||
|
readPtr = 0;
|
||||||
|
writePtr = 0;
|
||||||
|
filled = false;
|
||||||
|
return src->seek(pos, dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioFileSourceSPIRAMBuffer::close()
|
||||||
|
{
|
||||||
|
return src->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioFileSourceSPIRAMBuffer::isOpen()
|
||||||
|
{
|
||||||
|
return src->isOpen();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t AudioFileSourceSPIRAMBuffer::getSize()
|
||||||
|
{
|
||||||
|
return src->getSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t AudioFileSourceSPIRAMBuffer::getPos()
|
||||||
|
{
|
||||||
|
return src->getPos() - (writePtr - readPtr);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t AudioFileSourceSPIRAMBuffer::read(void *data, uint32_t len)
|
||||||
|
{
|
||||||
|
uint32_t bytes = 0;
|
||||||
|
|
||||||
|
// Check if the buffer isn't empty, otherwise we try to fill completely
|
||||||
|
if (!filled) {
|
||||||
|
cb.st(999, PSTR("Filling buffer..."));
|
||||||
|
uint8_t buffer[256];
|
||||||
|
writePtr = 0;
|
||||||
|
readPtr = 0;
|
||||||
|
// Fill up completely before returning any data at all
|
||||||
|
do {
|
||||||
|
int toRead = std::min(ramSize - (writePtr - readPtr), sizeof(buffer));
|
||||||
|
int length = src->read(buffer, toRead);
|
||||||
|
if (length > 0) {
|
||||||
|
#ifdef FAKERAM
|
||||||
|
for (size_t i=0; i<length; i++) fakeRAM[(i+writePtr)%ramSize] = buffer[i];
|
||||||
|
#else
|
||||||
|
ram.writeBytes(writePtr % ramSize, buffer, length);
|
||||||
|
#endif
|
||||||
|
writePtr += length;
|
||||||
|
} else {
|
||||||
|
// EOF, break out of read loop
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} while ((writePtr - readPtr) < ramSize);
|
||||||
|
filled = true;
|
||||||
|
cb.st(999, PSTR("Buffer filled..."));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read up to the entire buffer from RAM
|
||||||
|
uint32_t toReadFromBuffer = std::min(len, writePtr - readPtr);
|
||||||
|
uint8_t *ptr = reinterpret_cast<uint8_t*>(data);
|
||||||
|
if (toReadFromBuffer > 0) {
|
||||||
|
#ifdef FAKERAM
|
||||||
|
for (size_t i=0; i<toReadFromBuffer; i++) ptr[i] = fakeRAM[(i+readPtr)%ramSize];
|
||||||
|
#else
|
||||||
|
ram.readBytes(readPtr % ramSize, ptr, toReadFromBuffer);
|
||||||
|
#endif
|
||||||
|
readPtr += toReadFromBuffer;
|
||||||
|
ptr += toReadFromBuffer;
|
||||||
|
bytes += toReadFromBuffer;
|
||||||
|
len -= toReadFromBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If len>0 there is no data left in buffer and we try to read more directly from source.
|
||||||
|
// Then, we trigger a complete buffer refill
|
||||||
|
if (len) {
|
||||||
|
bytes += src->read(data, len);
|
||||||
|
filled = false;
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioFileSourceSPIRAMBuffer::fill()
|
||||||
|
{
|
||||||
|
// Make sure the buffer is pre-filled before make partial fill.
|
||||||
|
if (!filled) return;
|
||||||
|
|
||||||
|
for (auto i=0; i<5; i++) {
|
||||||
|
// Make sure there is at least buffer size free in RAM
|
||||||
|
uint8_t buffer[128];
|
||||||
|
if ((ramSize - (writePtr - readPtr)) < sizeof(buffer)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int cnt = src->readNonBlock(buffer, sizeof(buffer));
|
||||||
|
if (cnt) {
|
||||||
|
#ifdef FAKERAM
|
||||||
|
for (size_t i=0; i<cnt; i++) fakeRAM[(i+writePtr)%ramSize] = buffer[i];
|
||||||
|
#else
|
||||||
|
ram.writeBytes(writePtr % ramSize, buffer, cnt);
|
||||||
|
#endif
|
||||||
|
writePtr += cnt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioFileSourceSPIRAMBuffer::loop()
|
||||||
|
{
|
||||||
|
static uint32_t last = 0;
|
||||||
|
if (!src->loop()) return false;
|
||||||
|
fill();
|
||||||
|
if ((ESP.getCycleCount() - last) > microsecondsToClockCycles(1000000)) {
|
||||||
|
last = ESP.getCycleCount();
|
||||||
|
char str[65];
|
||||||
|
memset(str, '#', 64);
|
||||||
|
str[64] = 0;
|
||||||
|
str[((writePtr - readPtr) * 64)/ramSize] = 0;
|
||||||
|
cb.st(((writePtr - readPtr) * 100)/ramSize, str);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
/*
|
||||||
|
AudioFileSourceSPIRAMBuffer
|
||||||
|
Buffered file source in external SPI RAM
|
||||||
|
|
||||||
|
Copyright (C) 2017 Sebastien Decourriere
|
||||||
|
Based on AudioFileSourceBuffer class from Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _AUDIOFILESOURCESPIRAMBUFFER_H
|
||||||
|
#define _AUDIOFILESOURCESPIRAMBUFFER_H
|
||||||
|
|
||||||
|
#include "AudioFileSource.h"
|
||||||
|
#include <SPI.h>
|
||||||
|
#include "spiram-fast.h"
|
||||||
|
//#define FAKERAM
|
||||||
|
// #define SPIBUF_DEBUG
|
||||||
|
|
||||||
|
class AudioFileSourceSPIRAMBuffer : public AudioFileSource
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
#ifdef FAKERAM
|
||||||
|
AudioFileSourceSPIRAMBuffer(AudioFileSource *in, uint8_t csPin = 15, uint32_t bufferBytes = 2048);
|
||||||
|
#else
|
||||||
|
AudioFileSourceSPIRAMBuffer(AudioFileSource *in, uint8_t csPin = 15, uint32_t bufferBytes = 128*1024);
|
||||||
|
#endif
|
||||||
|
virtual ~AudioFileSourceSPIRAMBuffer() override;
|
||||||
|
|
||||||
|
virtual uint32_t read(void *data, uint32_t len) override;
|
||||||
|
virtual bool seek(int32_t pos, int dir) override;
|
||||||
|
virtual bool close() override;
|
||||||
|
virtual bool isOpen() override;
|
||||||
|
virtual uint32_t getSize() override;
|
||||||
|
virtual uint32_t getPos() override;
|
||||||
|
virtual bool loop() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
virtual void fill();
|
||||||
|
|
||||||
|
private:
|
||||||
|
AudioFileSource *src;
|
||||||
|
ESP8266SPIRAM ram;
|
||||||
|
size_t ramSize;
|
||||||
|
size_t writePtr;
|
||||||
|
size_t readPtr;
|
||||||
|
bool filled;
|
||||||
|
|
||||||
|
#ifdef FAKERAM
|
||||||
|
char fakeRAM[2048];
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
|
@ -0,0 +1,98 @@
|
||||||
|
/*
|
||||||
|
AudioFileSourceSTDIO
|
||||||
|
Input STDIO "file" to be used by AudioGenerator
|
||||||
|
Only for hot-based testing
|
||||||
|
|
||||||
|
Copyright (C) 2017 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#ifndef ARDUINO
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
#include "AudioFileSourceSTDIO.h"
|
||||||
|
|
||||||
|
AudioFileSourceSTDIO::AudioFileSourceSTDIO()
|
||||||
|
{
|
||||||
|
f = NULL;
|
||||||
|
srand(time(NULL));
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioFileSourceSTDIO::AudioFileSourceSTDIO(const char *filename)
|
||||||
|
{
|
||||||
|
open(filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioFileSourceSTDIO::open(const char *filename)
|
||||||
|
{
|
||||||
|
f = fopen(filename, "rb");
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioFileSourceSTDIO::~AudioFileSourceSTDIO()
|
||||||
|
{
|
||||||
|
if (f) fclose(f);
|
||||||
|
f = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t AudioFileSourceSTDIO::read(void *data, uint32_t len)
|
||||||
|
{
|
||||||
|
// if (rand() % 100 == 69) { // Give 0 data 1%
|
||||||
|
// printf("0 read\n");
|
||||||
|
// len = 0;
|
||||||
|
// } else if (rand() % 100 == 1) { // Give short reads 1%
|
||||||
|
// printf("0 read\n");
|
||||||
|
// len = 0;
|
||||||
|
// }
|
||||||
|
int ret = fread(reinterpret_cast<uint8_t*>(data), 1, len, f);
|
||||||
|
// if (ret && rand() % 100 < 5 ) {
|
||||||
|
// // We're really mean...throw bad data in the mix
|
||||||
|
// printf("bad data\n");
|
||||||
|
// for (int i=0; i<100; i++)
|
||||||
|
// *(reinterpret_cast<uint8_t*>(data) + (rand() % ret)) = rand();
|
||||||
|
// }
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioFileSourceSTDIO::seek(int32_t pos, int dir)
|
||||||
|
{
|
||||||
|
return fseek(f, pos, dir) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioFileSourceSTDIO::close()
|
||||||
|
{
|
||||||
|
fclose(f);
|
||||||
|
f = NULL;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioFileSourceSTDIO::isOpen()
|
||||||
|
{
|
||||||
|
return f?true:false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t AudioFileSourceSTDIO::getSize()
|
||||||
|
{
|
||||||
|
if (!f) return 0;
|
||||||
|
uint32_t p = ftell(f);
|
||||||
|
fseek(f, 0, SEEK_END);
|
||||||
|
uint32_t len = ftell(f);
|
||||||
|
fseek(f, p, SEEK_SET);
|
||||||
|
return len;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#endif
|
|
@ -0,0 +1,53 @@
|
||||||
|
/*
|
||||||
|
AudioFileSourceSTDIO
|
||||||
|
Input SPIFFS "file" to be used by AudioGenerator
|
||||||
|
Only for host-based testing, not Arduino
|
||||||
|
|
||||||
|
Copyright (C) 2017 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _AUDIOFILESOURCESTDIO_H
|
||||||
|
#define _AUDIOFILESOURCESTDIO_H
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
#ifndef ARDUINO
|
||||||
|
|
||||||
|
#include "AudioFileSource.h"
|
||||||
|
|
||||||
|
class AudioFileSourceSTDIO : public AudioFileSource
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
AudioFileSourceSTDIO();
|
||||||
|
AudioFileSourceSTDIO(const char *filename);
|
||||||
|
virtual ~AudioFileSourceSTDIO() override;
|
||||||
|
|
||||||
|
virtual bool open(const char *filename) override;
|
||||||
|
virtual uint32_t read(void *data, uint32_t len) override;
|
||||||
|
virtual bool seek(int32_t pos, int dir) override;
|
||||||
|
virtual bool close() override;
|
||||||
|
virtual bool isOpen() override;
|
||||||
|
virtual uint32_t getSize() override;
|
||||||
|
virtual uint32_t getPos() override { if (!f) return 0; else return (uint32_t)ftell(f); };
|
||||||
|
|
||||||
|
private:
|
||||||
|
FILE *f;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // !ARDUINO
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include "AudioFileStream.h"
|
||||||
|
|
||||||
|
|
||||||
|
AudioFileStream::AudioFileStream(AudioFileSource *source, int definedLen)
|
||||||
|
{
|
||||||
|
src = source;
|
||||||
|
len = definedLen;
|
||||||
|
ptr = 0;
|
||||||
|
saved = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioFileStream::~AudioFileStream()
|
||||||
|
{
|
||||||
|
// If there's a defined len, read until we're empty
|
||||||
|
if (len) {
|
||||||
|
while (ptr++ < len) (void)read();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int AudioFileStream::available()
|
||||||
|
{
|
||||||
|
if (saved >= 0) return 1;
|
||||||
|
else if (len) return ptr - len;
|
||||||
|
else if (src->getSize()) return (src->getPos() - src->getSize());
|
||||||
|
else return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int AudioFileStream::read()
|
||||||
|
{
|
||||||
|
uint8_t c;
|
||||||
|
int r;
|
||||||
|
if (ptr >= len) return -1;
|
||||||
|
ptr++;
|
||||||
|
if (saved >= 0) {
|
||||||
|
c = (uint8_t)saved;
|
||||||
|
saved = -1;
|
||||||
|
r = 1;
|
||||||
|
} else {
|
||||||
|
r = src->read(&c, 1);
|
||||||
|
}
|
||||||
|
if (r != 1) return -1;
|
||||||
|
return (int)c;
|
||||||
|
}
|
||||||
|
|
||||||
|
int AudioFileStream::peek()
|
||||||
|
{
|
||||||
|
uint8_t c;
|
||||||
|
if ((ptr+1) >= len) return -1;
|
||||||
|
if (saved >= 0) return saved;
|
||||||
|
int r = src->read(&c, 1);
|
||||||
|
if (r<1) return -1;
|
||||||
|
saved = c;
|
||||||
|
return saved;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioFileStream::flush()
|
||||||
|
{
|
||||||
|
/* noop? */
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
/*
|
||||||
|
AudioFileStream
|
||||||
|
Convert an AudioFileSource* to a Stream*
|
||||||
|
|
||||||
|
Copyright (C) 2017 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef AUDIOFILESTREAM_H
|
||||||
|
#define AUDIOFILESTREAM_H
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include "AudioFileSource.h"
|
||||||
|
|
||||||
|
class AudioFileStream : public Stream
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
AudioFileStream(AudioFileSource *source, int definedLen);
|
||||||
|
virtual ~AudioFileStream();
|
||||||
|
|
||||||
|
public:
|
||||||
|
// Stream interface - see the Arduino library documentation.
|
||||||
|
virtual int available() override;
|
||||||
|
virtual int read() override;
|
||||||
|
virtual int peek() override;
|
||||||
|
virtual void flush() override;
|
||||||
|
virtual size_t write(uint8_t x) override { (void)x; return 0; };
|
||||||
|
|
||||||
|
private:
|
||||||
|
AudioFileSource *src;
|
||||||
|
int saved;
|
||||||
|
int len;
|
||||||
|
int ptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
|
@ -0,0 +1,53 @@
|
||||||
|
/*
|
||||||
|
AudioGenerator
|
||||||
|
Base class of an audio output generator
|
||||||
|
|
||||||
|
Copyright (C) 2017 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _AUDIOGENERATOR_H
|
||||||
|
#define _AUDIOGENERATOR_H
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include "AudioStatus.h"
|
||||||
|
#include "AudioFileSource.h"
|
||||||
|
#include "AudioOutput.h"
|
||||||
|
|
||||||
|
class AudioGenerator
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
AudioGenerator() { lastSample[0] = 0; lastSample[1] = 0; };
|
||||||
|
virtual ~AudioGenerator() {};
|
||||||
|
virtual bool begin(AudioFileSource *source, AudioOutput *output) { (void)source; (void)output; return false; };
|
||||||
|
virtual bool loop() { return false; };
|
||||||
|
virtual bool stop() { return false; };
|
||||||
|
virtual bool isRunning() { return false;};
|
||||||
|
|
||||||
|
public:
|
||||||
|
virtual bool RegisterMetadataCB(AudioStatus::metadataCBFn fn, void *data) { return cb.RegisterMetadataCB(fn, data); }
|
||||||
|
virtual bool RegisterStatusCB(AudioStatus::statusCBFn fn, void *data) { return cb.RegisterStatusCB(fn, data); }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool running;
|
||||||
|
AudioFileSource *file;
|
||||||
|
AudioOutput *output;
|
||||||
|
int16_t lastSample[2];
|
||||||
|
|
||||||
|
protected:
|
||||||
|
AudioStatus cb;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
|
@ -0,0 +1,214 @@
|
||||||
|
/*
|
||||||
|
AudioGeneratorAAC
|
||||||
|
Audio output generator using the Helix AAC decoder
|
||||||
|
|
||||||
|
Copyright (C) 2017 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma GCC optimize ("O3")
|
||||||
|
|
||||||
|
#include "AudioGeneratorAAC.h"
|
||||||
|
|
||||||
|
AudioGeneratorAAC::AudioGeneratorAAC()
|
||||||
|
{
|
||||||
|
preallocateSpace = NULL;
|
||||||
|
preallocateSize = 0;
|
||||||
|
|
||||||
|
running = false;
|
||||||
|
file = NULL;
|
||||||
|
output = NULL;
|
||||||
|
|
||||||
|
buff = (uint8_t*)malloc(buffLen);
|
||||||
|
outSample = (int16_t*)malloc(1024 * 2 * sizeof(uint16_t));
|
||||||
|
if (!buff || !outSample) {
|
||||||
|
audioLogger->printf_P(PSTR("ERROR: Out of memory in AAC\n"));
|
||||||
|
Serial.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
hAACDecoder = AACInitDecoder();
|
||||||
|
if (!hAACDecoder) {
|
||||||
|
audioLogger->printf_P(PSTR("Out of memory error! hAACDecoder==NULL\n"));
|
||||||
|
Serial.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
buffValid = 0;
|
||||||
|
lastFrameEnd = 0;
|
||||||
|
validSamples = 0;
|
||||||
|
curSample = 0;
|
||||||
|
lastRate = 0;
|
||||||
|
lastChannels = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioGeneratorAAC::AudioGeneratorAAC(void *preallocateData, int preallocateSz)
|
||||||
|
{
|
||||||
|
preallocateSpace = preallocateData;
|
||||||
|
preallocateSize = preallocateSz;
|
||||||
|
|
||||||
|
running = false;
|
||||||
|
file = NULL;
|
||||||
|
output = NULL;
|
||||||
|
|
||||||
|
uint8_t *p = (uint8_t*)preallocateSpace;
|
||||||
|
buff = (uint8_t*) p;
|
||||||
|
p += (buffLen + 7) & ~7;
|
||||||
|
outSample = (int16_t*) p;
|
||||||
|
p += (1024 * 2 * sizeof(int16_t) + 7) & ~7;
|
||||||
|
int used = p - (uint8_t*)preallocateSpace;
|
||||||
|
int availSpace = preallocateSize - used;
|
||||||
|
if (availSpace < 0 ) {
|
||||||
|
audioLogger->printf_P(PSTR("ERROR: Out of memory in AAC\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
hAACDecoder = AACInitDecoderPre(p, availSpace);
|
||||||
|
if (!hAACDecoder) {
|
||||||
|
audioLogger->printf_P(PSTR("Out of memory error! hAACDecoder==NULL\n"));
|
||||||
|
Serial.flush();
|
||||||
|
}
|
||||||
|
buffValid = 0;
|
||||||
|
lastFrameEnd = 0;
|
||||||
|
validSamples = 0;
|
||||||
|
curSample = 0;
|
||||||
|
lastRate = 0;
|
||||||
|
lastChannels = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
AudioGeneratorAAC::~AudioGeneratorAAC()
|
||||||
|
{
|
||||||
|
if (!preallocateSpace) {
|
||||||
|
AACFreeDecoder(hAACDecoder);
|
||||||
|
free(buff);
|
||||||
|
free(outSample);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioGeneratorAAC::stop()
|
||||||
|
{
|
||||||
|
running = false;
|
||||||
|
output->stop();
|
||||||
|
return file->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioGeneratorAAC::isRunning()
|
||||||
|
{
|
||||||
|
return running;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioGeneratorAAC::FillBufferWithValidFrame()
|
||||||
|
{
|
||||||
|
buff[0] = 0; // Destroy any existing sync word @ 0
|
||||||
|
int nextSync;
|
||||||
|
do {
|
||||||
|
nextSync = AACFindSyncWord(buff + lastFrameEnd, buffValid - lastFrameEnd);
|
||||||
|
if (nextSync >= 0) nextSync += lastFrameEnd;
|
||||||
|
lastFrameEnd = 0;
|
||||||
|
if (nextSync == -1) {
|
||||||
|
if (buffValid && buff[buffValid-1]==0xff) { // Could be 1st half of syncword, preserve it...
|
||||||
|
buff[0] = 0xff;
|
||||||
|
buffValid = file->read(buff+1, buffLen-1);
|
||||||
|
if (buffValid==0) return false; // No data available, EOF
|
||||||
|
} else { // Try a whole new buffer
|
||||||
|
buffValid = file->read(buff, buffLen-1);
|
||||||
|
if (buffValid==0) return false; // No data available, EOF
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (nextSync == -1);
|
||||||
|
|
||||||
|
// Move the frame to start at offset 0 in the buffer
|
||||||
|
buffValid -= nextSync; // Throw out prior to nextSync
|
||||||
|
memmove(buff, buff+nextSync, buffValid);
|
||||||
|
|
||||||
|
// We have a sync word at 0 now, try and fill remainder of buffer
|
||||||
|
buffValid += file->read(buff + buffValid, buffLen - buffValid);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioGeneratorAAC::loop()
|
||||||
|
{
|
||||||
|
if (!running) goto done; // Nothing to do here!
|
||||||
|
|
||||||
|
// If we've got data, try and pump it out...
|
||||||
|
while (validSamples) {
|
||||||
|
lastSample[0] = outSample[curSample*2];
|
||||||
|
lastSample[1] = outSample[curSample*2 + 1];
|
||||||
|
if (!output->ConsumeSample(lastSample)) goto done; // Can't send, but no error detected
|
||||||
|
validSamples--;
|
||||||
|
curSample++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No samples available, need to decode a new frame
|
||||||
|
if (FillBufferWithValidFrame()) {
|
||||||
|
// buff[0] start of frame, decode it...
|
||||||
|
unsigned char *inBuff = reinterpret_cast<unsigned char *>(buff);
|
||||||
|
int bytesLeft = buffValid;
|
||||||
|
int ret = AACDecode(hAACDecoder, &inBuff, &bytesLeft, outSample);
|
||||||
|
if (ret) {
|
||||||
|
// Error, skip the frame...
|
||||||
|
char buff[48];
|
||||||
|
sprintf_P(buff, PSTR("AAC decode error %d"), ret);
|
||||||
|
cb.st(ret, buff);
|
||||||
|
} else {
|
||||||
|
lastFrameEnd = buffValid - bytesLeft;
|
||||||
|
AACFrameInfo fi;
|
||||||
|
AACGetLastFrameInfo(hAACDecoder, &fi);
|
||||||
|
if ((int)fi.sampRateOut != (int)lastRate) {
|
||||||
|
output->SetRate(fi.sampRateOut);
|
||||||
|
lastRate = fi.sampRateOut;
|
||||||
|
}
|
||||||
|
if (fi.nChans != lastChannels) {
|
||||||
|
output->SetChannels(fi.nChans);
|
||||||
|
lastChannels = fi.nChans;
|
||||||
|
}
|
||||||
|
curSample = 0;
|
||||||
|
validSamples = fi.outputSamps / lastChannels;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
running = false; // No more data, we're done here...
|
||||||
|
}
|
||||||
|
|
||||||
|
done:
|
||||||
|
file->loop();
|
||||||
|
output->loop();
|
||||||
|
|
||||||
|
return running;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioGeneratorAAC::begin(AudioFileSource *source, AudioOutput *output)
|
||||||
|
{
|
||||||
|
if (!source) return false;
|
||||||
|
file = source;
|
||||||
|
if (!output) return false;
|
||||||
|
this->output = output;
|
||||||
|
if (!file->isOpen()) return false; // Error
|
||||||
|
|
||||||
|
output->begin();
|
||||||
|
|
||||||
|
// AAC always comes out at 16 bits
|
||||||
|
output->SetBitsPerSample(16);
|
||||||
|
|
||||||
|
|
||||||
|
memset(buff, 0, buffLen);
|
||||||
|
memset(outSample, 0, 1024*2*sizeof(int16_t));
|
||||||
|
|
||||||
|
|
||||||
|
running = true;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
/*
|
||||||
|
AudioGeneratorAAC
|
||||||
|
Audio output generator using the Helix AAC decoder
|
||||||
|
|
||||||
|
Copyright (C) 2017 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _AUDIOGENERATORAAC_H
|
||||||
|
#define _AUDIOGENERATORAAC_H
|
||||||
|
|
||||||
|
#include "AudioGenerator.h"
|
||||||
|
#include "libhelix-aac/aacdec.h"
|
||||||
|
|
||||||
|
class AudioGeneratorAAC : public AudioGenerator
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
AudioGeneratorAAC();
|
||||||
|
AudioGeneratorAAC(void *preallocateData, int preallocateSize);
|
||||||
|
virtual ~AudioGeneratorAAC() override;
|
||||||
|
virtual bool begin(AudioFileSource *source, AudioOutput *output) override;
|
||||||
|
virtual bool loop() override;
|
||||||
|
virtual bool stop() override;
|
||||||
|
virtual bool isRunning() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void *preallocateSpace;
|
||||||
|
int preallocateSize;
|
||||||
|
|
||||||
|
// Helix AAC decoder
|
||||||
|
HAACDecoder hAACDecoder;
|
||||||
|
|
||||||
|
// Input buffering
|
||||||
|
const int buffLen = 1600;
|
||||||
|
uint8_t *buff; //[1600]; // File buffer required to store at least a whole compressed frame
|
||||||
|
int16_t buffValid;
|
||||||
|
int16_t lastFrameEnd;
|
||||||
|
bool FillBufferWithValidFrame(); // Read until we get a valid syncword and min(feof, 2048) butes in the buffer
|
||||||
|
|
||||||
|
// Output buffering
|
||||||
|
int16_t *outSample; //[1024 * 2]; // Interleaved L/R
|
||||||
|
int16_t validSamples;
|
||||||
|
int16_t curSample;
|
||||||
|
|
||||||
|
// Each frame may change this if they're very strange, I guess
|
||||||
|
unsigned int lastRate;
|
||||||
|
int lastChannels;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
|
@ -0,0 +1,199 @@
|
||||||
|
/*
|
||||||
|
AudioGeneratorFLAC
|
||||||
|
Audio output generator that plays FLAC audio files
|
||||||
|
|
||||||
|
Copyright (C) 2017 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <AudioGeneratorFLAC.h>
|
||||||
|
|
||||||
|
AudioGeneratorFLAC::AudioGeneratorFLAC()
|
||||||
|
{
|
||||||
|
flac = NULL;
|
||||||
|
channels = 0;
|
||||||
|
sampleRate = 0;
|
||||||
|
bitsPerSample = 0;
|
||||||
|
buff[0] = NULL;
|
||||||
|
buff[1] = NULL;
|
||||||
|
buffPtr = 0;
|
||||||
|
buffLen = 0;
|
||||||
|
running = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioGeneratorFLAC::~AudioGeneratorFLAC()
|
||||||
|
{
|
||||||
|
if (flac)
|
||||||
|
FLAC__stream_decoder_delete(flac);
|
||||||
|
flac = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioGeneratorFLAC::begin(AudioFileSource *source, AudioOutput *output)
|
||||||
|
{
|
||||||
|
if (!source) return false;
|
||||||
|
file = source;
|
||||||
|
if (!output) return false;
|
||||||
|
this->output = output;
|
||||||
|
if (!file->isOpen()) return false; // Error
|
||||||
|
|
||||||
|
flac = FLAC__stream_decoder_new();
|
||||||
|
if (!flac) return false;
|
||||||
|
|
||||||
|
(void)FLAC__stream_decoder_set_md5_checking(flac, false);
|
||||||
|
|
||||||
|
FLAC__StreamDecoderInitStatus ret = FLAC__stream_decoder_init_stream(flac, _read_cb, _seek_cb, _tell_cb, _length_cb, _eof_cb, _write_cb, _metadata_cb, _error_cb, reinterpret_cast<void*>(this) );
|
||||||
|
if (ret != FLAC__STREAM_DECODER_INIT_STATUS_OK) {
|
||||||
|
FLAC__stream_decoder_delete(flac);
|
||||||
|
flac = NULL;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
output->begin();
|
||||||
|
running = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioGeneratorFLAC::loop()
|
||||||
|
{
|
||||||
|
FLAC__bool ret;
|
||||||
|
|
||||||
|
if (!running) goto done;
|
||||||
|
|
||||||
|
if (!output->ConsumeSample(lastSample)) goto done; // Try and send last buffered sample
|
||||||
|
|
||||||
|
do {
|
||||||
|
if (buffPtr == buffLen) {
|
||||||
|
ret = FLAC__stream_decoder_process_single(flac);
|
||||||
|
if (!ret) {
|
||||||
|
running = false;
|
||||||
|
goto done;
|
||||||
|
} else {
|
||||||
|
// We might be done...
|
||||||
|
if (FLAC__stream_decoder_get_state(flac)==FLAC__STREAM_DECODER_END_OF_STREAM) {
|
||||||
|
running = false;
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
unsigned newsr = FLAC__stream_decoder_get_sample_rate(flac);
|
||||||
|
unsigned newch = FLAC__stream_decoder_get_channels(flac);
|
||||||
|
unsigned newbps = FLAC__stream_decoder_get_bits_per_sample(flac);
|
||||||
|
if (newsr != sampleRate) output->SetRate(sampleRate = newsr);
|
||||||
|
if (newch != channels) output->SetChannels(channels = newch);
|
||||||
|
if (newbps != bitsPerSample) output->SetBitsPerSample( bitsPerSample = newbps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for some weird case where above didn't give any data
|
||||||
|
if (buffPtr == buffLen) {
|
||||||
|
goto done; // At some point the flac better error and we'll return
|
||||||
|
}
|
||||||
|
if (bitsPerSample <= 16) {
|
||||||
|
lastSample[AudioOutput::LEFTCHANNEL] = buff[0][buffPtr] & 0xffff;
|
||||||
|
if (channels==2) lastSample[AudioOutput::RIGHTCHANNEL] = buff[1][buffPtr] & 0xffff;
|
||||||
|
else lastSample[AudioOutput::RIGHTCHANNEL] = lastSample[AudioOutput::LEFTCHANNEL];
|
||||||
|
} else if (bitsPerSample <= 24) {
|
||||||
|
lastSample[AudioOutput::LEFTCHANNEL] = (buff[0][buffPtr]>>8) & 0xffff;
|
||||||
|
if (channels==2) lastSample[AudioOutput::RIGHTCHANNEL] = (buff[1][buffPtr]>>8) & 0xffff;
|
||||||
|
else lastSample[AudioOutput::RIGHTCHANNEL] = lastSample[AudioOutput::LEFTCHANNEL];
|
||||||
|
} else {
|
||||||
|
lastSample[AudioOutput::LEFTCHANNEL] = (buff[0][buffPtr]>>16) & 0xffff;
|
||||||
|
if (channels==2) lastSample[AudioOutput::RIGHTCHANNEL] = (buff[1][buffPtr]>>16) & 0xffff;
|
||||||
|
else lastSample[AudioOutput::RIGHTCHANNEL] = lastSample[AudioOutput::LEFTCHANNEL];
|
||||||
|
}
|
||||||
|
buffPtr++;
|
||||||
|
} while (running && output->ConsumeSample(lastSample));
|
||||||
|
|
||||||
|
done:
|
||||||
|
file->loop();
|
||||||
|
output->loop();
|
||||||
|
|
||||||
|
return running;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioGeneratorFLAC::stop()
|
||||||
|
{
|
||||||
|
if (flac)
|
||||||
|
FLAC__stream_decoder_delete(flac);
|
||||||
|
flac = NULL;
|
||||||
|
running = false;
|
||||||
|
output->stop();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioGeneratorFLAC::isRunning()
|
||||||
|
{
|
||||||
|
return running;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
FLAC__StreamDecoderReadStatus AudioGeneratorFLAC::read_cb(const FLAC__StreamDecoder *decoder, FLAC__byte buffer[], size_t *bytes)
|
||||||
|
{
|
||||||
|
(void) decoder;
|
||||||
|
if (*bytes==0) return FLAC__STREAM_DECODER_READ_STATUS_ABORT;
|
||||||
|
*bytes = file->read(buffer, sizeof(FLAC__byte) * (*bytes));
|
||||||
|
if (*bytes==0) return FLAC__STREAM_DECODER_READ_STATUS_END_OF_STREAM;
|
||||||
|
return FLAC__STREAM_DECODER_READ_STATUS_CONTINUE;
|
||||||
|
}
|
||||||
|
FLAC__StreamDecoderSeekStatus AudioGeneratorFLAC::seek_cb(const FLAC__StreamDecoder *decoder, FLAC__uint64 absolute_byte_offset)
|
||||||
|
{
|
||||||
|
(void) decoder;
|
||||||
|
if (!file->seek((int32_t)absolute_byte_offset, 0)) return FLAC__STREAM_DECODER_SEEK_STATUS_ERROR;
|
||||||
|
return FLAC__STREAM_DECODER_SEEK_STATUS_OK;
|
||||||
|
}
|
||||||
|
FLAC__StreamDecoderTellStatus AudioGeneratorFLAC::tell_cb(const FLAC__StreamDecoder *decoder, FLAC__uint64 *absolute_byte_offset)
|
||||||
|
{
|
||||||
|
(void) decoder;
|
||||||
|
*absolute_byte_offset = file->getPos();
|
||||||
|
return FLAC__STREAM_DECODER_TELL_STATUS_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
FLAC__StreamDecoderLengthStatus AudioGeneratorFLAC::length_cb(const FLAC__StreamDecoder *decoder, FLAC__uint64 *stream_length)
|
||||||
|
{
|
||||||
|
(void) decoder;
|
||||||
|
*stream_length = file->getSize();
|
||||||
|
return FLAC__STREAM_DECODER_LENGTH_STATUS_OK;
|
||||||
|
}
|
||||||
|
FLAC__bool AudioGeneratorFLAC::eof_cb(const FLAC__StreamDecoder *decoder)
|
||||||
|
{
|
||||||
|
(void) decoder;
|
||||||
|
if (file->getPos() >= file->getSize()) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
FLAC__StreamDecoderWriteStatus AudioGeneratorFLAC::write_cb(const FLAC__StreamDecoder *decoder, const FLAC__Frame *frame, const FLAC__int32 *const buffer[])
|
||||||
|
{
|
||||||
|
(void) decoder;
|
||||||
|
// Hackish warning here. FLAC sends the buffer but doesn't free it until the next call to decode_frame, so we stash
|
||||||
|
// the pointers here and use it in our loop() instead of memcpy()'ing into yet another buffer.
|
||||||
|
buffLen = frame->header.blocksize;
|
||||||
|
buff[0] = buffer[0];
|
||||||
|
if (frame->header.channels>1) buff[1] = buffer[1];
|
||||||
|
else buff[1] = buffer[0];
|
||||||
|
buffPtr = 0;
|
||||||
|
return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE;
|
||||||
|
}
|
||||||
|
void AudioGeneratorFLAC::metadata_cb(const FLAC__StreamDecoder *decoder, const FLAC__StreamMetadata *metadata)
|
||||||
|
{
|
||||||
|
(void) decoder;
|
||||||
|
(void) metadata;
|
||||||
|
audioLogger->printf_P(PSTR("Metadata\n"));
|
||||||
|
}
|
||||||
|
char AudioGeneratorFLAC::error_cb_str[64];
|
||||||
|
void AudioGeneratorFLAC::error_cb(const FLAC__StreamDecoder *decoder, FLAC__StreamDecoderErrorStatus status)
|
||||||
|
{
|
||||||
|
(void) decoder;
|
||||||
|
strncpy_P(error_cb_str, FLAC__StreamDecoderErrorStatusString[status], 64);
|
||||||
|
cb.st((int)status, error_cb_str);
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
/*
|
||||||
|
AudioGeneratorFLAC
|
||||||
|
Audio output generator that plays FLAC audio files
|
||||||
|
|
||||||
|
Copyright (C) 2017 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _AUDIOGENERATORFLAC_H
|
||||||
|
#define _AUDIOGENERATORFLAC_H
|
||||||
|
|
||||||
|
#include <AudioGenerator.h>
|
||||||
|
extern "C" {
|
||||||
|
#include "libflac/FLAC/stream_decoder.h"
|
||||||
|
};
|
||||||
|
|
||||||
|
class AudioGeneratorFLAC : public AudioGenerator
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
AudioGeneratorFLAC();
|
||||||
|
virtual ~AudioGeneratorFLAC() override;
|
||||||
|
virtual bool begin(AudioFileSource *source, AudioOutput *output) override;
|
||||||
|
virtual bool loop() override;
|
||||||
|
virtual bool stop() override;
|
||||||
|
virtual bool isRunning() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// FLAC info
|
||||||
|
uint16_t channels;
|
||||||
|
uint32_t sampleRate;
|
||||||
|
uint16_t bitsPerSample;
|
||||||
|
|
||||||
|
// We need to buffer some data in-RAM to avoid doing 1000s of small reads
|
||||||
|
const int *buff[2];
|
||||||
|
uint16_t buffPtr;
|
||||||
|
uint16_t buffLen;
|
||||||
|
FLAC__StreamDecoder *flac;
|
||||||
|
|
||||||
|
// FLAC callbacks, need static functions to bounce into c++ from c
|
||||||
|
static FLAC__StreamDecoderReadStatus _read_cb(const FLAC__StreamDecoder *decoder, FLAC__byte buffer[], size_t *bytes, void *client_data) {
|
||||||
|
return static_cast<AudioGeneratorFLAC*>(client_data)->read_cb(decoder, buffer, bytes);
|
||||||
|
};
|
||||||
|
static FLAC__StreamDecoderSeekStatus _seek_cb(const FLAC__StreamDecoder *decoder, FLAC__uint64 absolute_byte_offset, void *client_data) {
|
||||||
|
return static_cast<AudioGeneratorFLAC*>(client_data)->seek_cb(decoder, absolute_byte_offset);
|
||||||
|
};
|
||||||
|
static FLAC__StreamDecoderTellStatus _tell_cb(const FLAC__StreamDecoder *decoder, FLAC__uint64 *absolute_byte_offset, void *client_data) {
|
||||||
|
return static_cast<AudioGeneratorFLAC*>(client_data)->tell_cb(decoder, absolute_byte_offset);
|
||||||
|
};
|
||||||
|
static FLAC__StreamDecoderLengthStatus _length_cb(const FLAC__StreamDecoder *decoder, FLAC__uint64 *stream_length, void *client_data) {
|
||||||
|
return static_cast<AudioGeneratorFLAC*>(client_data)->length_cb(decoder, stream_length);
|
||||||
|
};
|
||||||
|
static FLAC__bool _eof_cb(const FLAC__StreamDecoder *decoder, void *client_data) {
|
||||||
|
return static_cast<AudioGeneratorFLAC*>(client_data)->eof_cb(decoder);
|
||||||
|
};
|
||||||
|
static FLAC__StreamDecoderWriteStatus _write_cb(const FLAC__StreamDecoder *decoder, const FLAC__Frame *frame, const FLAC__int32 *const buffer[], void *client_data) {
|
||||||
|
return static_cast<AudioGeneratorFLAC*>(client_data)->write_cb(decoder, frame, buffer);
|
||||||
|
};
|
||||||
|
static void _metadata_cb(const FLAC__StreamDecoder *decoder, const FLAC__StreamMetadata *metadata, void *client_data) {
|
||||||
|
static_cast<AudioGeneratorFLAC*>(client_data)->metadata_cb(decoder, metadata);
|
||||||
|
};
|
||||||
|
static void _error_cb(const FLAC__StreamDecoder *decoder, FLAC__StreamDecoderErrorStatus status, void *client_data) {
|
||||||
|
static_cast<AudioGeneratorFLAC*>(client_data)->error_cb(decoder, status);
|
||||||
|
};
|
||||||
|
// Actual FLAC callbacks
|
||||||
|
FLAC__StreamDecoderReadStatus read_cb(const FLAC__StreamDecoder *decoder, FLAC__byte buffer[], size_t *bytes);
|
||||||
|
FLAC__StreamDecoderSeekStatus seek_cb(const FLAC__StreamDecoder *decoder, FLAC__uint64 absolute_byte_offset);
|
||||||
|
FLAC__StreamDecoderTellStatus tell_cb(const FLAC__StreamDecoder *decoder, FLAC__uint64 *absolute_byte_offset);
|
||||||
|
FLAC__StreamDecoderLengthStatus length_cb(const FLAC__StreamDecoder *decoder, FLAC__uint64 *stream_length);
|
||||||
|
FLAC__bool eof_cb(const FLAC__StreamDecoder *decoder);
|
||||||
|
FLAC__StreamDecoderWriteStatus write_cb(const FLAC__StreamDecoder *decoder, const FLAC__Frame *frame, const FLAC__int32 *const buffer[]);
|
||||||
|
void metadata_cb(const FLAC__StreamDecoder *decoder, const FLAC__StreamMetadata *metadata);
|
||||||
|
static char error_cb_str[64];
|
||||||
|
void error_cb(const FLAC__StreamDecoder *decoder, FLAC__StreamDecoderErrorStatus status);
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
|
@ -0,0 +1,639 @@
|
||||||
|
/*
|
||||||
|
AudioGeneratorMIDI
|
||||||
|
Audio output generator that plays MIDI files using a SF2 SoundFont
|
||||||
|
|
||||||
|
Copyright (C) 2017 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
The MIDI processing engine is a heavily modified version of MIDITONES,
|
||||||
|
by Len Shustek, https://github.com/LenShustek/miditones .
|
||||||
|
Whereas MIDITONES original simply parsed a file beforehand to a byte
|
||||||
|
stream to be played by another program, this does the parsing and
|
||||||
|
playback in real-time.
|
||||||
|
|
||||||
|
Here's his original header/readme w/MIT license, which is subsumed by the
|
||||||
|
GPL license of the ESP8266Audio project.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
|
||||||
|
MIDITONES: Convert a MIDI file into a simple bytestream of notes
|
||||||
|
|
||||||
|
-------------------------------------------------------------------------
|
||||||
|
The MIT License (MIT)
|
||||||
|
Copyright (c) 2011,2013,2015,2016, Len Shustek
|
||||||
|
|
||||||
|
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.
|
||||||
|
**************************************************************************/
|
||||||
|
|
||||||
|
|
||||||
|
#include "AudioGeneratorMIDI.h"
|
||||||
|
|
||||||
|
#pragma GCC optimize ("O3")
|
||||||
|
|
||||||
|
#define TSF_NO_STDIO
|
||||||
|
#define TSF_IMPLEMENTATION
|
||||||
|
#include "libtinysoundfont/tsf.h"
|
||||||
|
|
||||||
|
/**************** utility routines **********************/
|
||||||
|
|
||||||
|
/* announce a fatal MIDI file format error */
|
||||||
|
|
||||||
|
void AudioGeneratorMIDI::midi_error(const char *msg, int curpos)
|
||||||
|
{
|
||||||
|
cb.st(curpos, msg);
|
||||||
|
#if 0
|
||||||
|
int ptr;
|
||||||
|
audioLogger->printf("---> MIDI file error at position %04X (%d): %s\n", (uint16_t) curpos, (uint16_t) curpos, msg);
|
||||||
|
/* print some bytes surrounding the error */
|
||||||
|
ptr = curpos - 16;
|
||||||
|
if (ptr < 0) ptr = 0;
|
||||||
|
buffer.seek( buffer.data, ptr );
|
||||||
|
for (int i = 0; i < 32; i++) {
|
||||||
|
char c;
|
||||||
|
buffer.read (buffer.data, &c, 1);
|
||||||
|
audioLogger->printf((ptr + i) == curpos ? " [%02X] " : "%02X ", (int) c & 0xff);
|
||||||
|
}
|
||||||
|
audioLogger->printf("\n");
|
||||||
|
#endif
|
||||||
|
running = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* check that we have a specified number of bytes left in the buffer */
|
||||||
|
|
||||||
|
void AudioGeneratorMIDI::chk_bufdata (int ptr, unsigned long int len) {
|
||||||
|
if ((unsigned) (ptr + len) > buflen)
|
||||||
|
midi_error ("data missing", ptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* fetch big-endian numbers */
|
||||||
|
|
||||||
|
uint16_t AudioGeneratorMIDI::rev_short (uint16_t val) {
|
||||||
|
return ((val & 0xff) << 8) | ((val >> 8) & 0xff);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t AudioGeneratorMIDI::rev_long (uint32_t val) {
|
||||||
|
return (((rev_short ((uint16_t) val) & 0xffff) << 16) |
|
||||||
|
(rev_short ((uint16_t) (val >> 16)) & 0xffff));
|
||||||
|
}
|
||||||
|
|
||||||
|
/************** process the MIDI file header *****************/
|
||||||
|
|
||||||
|
void AudioGeneratorMIDI::process_header (void) {
|
||||||
|
struct midi_header hdr;
|
||||||
|
unsigned int time_division;
|
||||||
|
|
||||||
|
chk_bufdata (hdrptr, sizeof (struct midi_header));
|
||||||
|
buffer.seek (buffer.data, hdrptr);
|
||||||
|
buffer.read (buffer.data, &hdr, sizeof (hdr));
|
||||||
|
if (!charcmp ((char *) hdr.MThd, "MThd"))
|
||||||
|
midi_error ("Missing 'MThd'", hdrptr);
|
||||||
|
num_tracks = rev_short (hdr.number_of_tracks);
|
||||||
|
time_division = rev_short (hdr.time_division);
|
||||||
|
if (time_division < 0x8000)
|
||||||
|
ticks_per_beat = time_division;
|
||||||
|
else
|
||||||
|
ticks_per_beat = ((time_division >> 8) & 0x7f) /* SMTE frames/sec */ *(time_division & 0xff); /* ticks/SMTE frame */
|
||||||
|
hdrptr += rev_long (hdr.header_size) + 8; /* point past header to track header, presumably. */
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**************** Process a MIDI track header *******************/
|
||||||
|
|
||||||
|
void AudioGeneratorMIDI::start_track (int tracknum) {
|
||||||
|
struct track_header hdr;
|
||||||
|
unsigned long tracklen;
|
||||||
|
|
||||||
|
chk_bufdata (hdrptr, sizeof (struct track_header));
|
||||||
|
buffer.seek (buffer.data, hdrptr);
|
||||||
|
buffer.read (buffer.data, &hdr, sizeof (hdr));
|
||||||
|
if (!charcmp ((char *) (hdr.MTrk), "MTrk"))
|
||||||
|
midi_error ("Missing 'MTrk'", hdrptr);
|
||||||
|
tracklen = rev_long (hdr.track_size);
|
||||||
|
hdrptr += sizeof (struct track_header); /* point past header */
|
||||||
|
chk_bufdata (hdrptr, tracklen);
|
||||||
|
track[tracknum].trkptr = hdrptr;
|
||||||
|
hdrptr += tracklen; /* point to the start of the next track */
|
||||||
|
track[tracknum].trkend = hdrptr; /* the point past the end of the track */
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned char AudioGeneratorMIDI::buffer_byte (int offset) {
|
||||||
|
unsigned char c;
|
||||||
|
buffer.seek (buffer.data, offset);
|
||||||
|
buffer.read (buffer.data, &c, 1);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned short AudioGeneratorMIDI::buffer_short (int offset) {
|
||||||
|
unsigned short s;
|
||||||
|
buffer.seek (buffer.data, offset);
|
||||||
|
buffer.read (buffer.data, &s, sizeof (short));
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned int AudioGeneratorMIDI::buffer_int32 (int offset) {
|
||||||
|
uint32_t i;
|
||||||
|
buffer.seek (buffer.data, offset);
|
||||||
|
buffer.read (buffer.data, &i, sizeof (i));
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Get a MIDI-style variable-length integer */
|
||||||
|
|
||||||
|
unsigned long AudioGeneratorMIDI::get_varlen (int *ptr) {
|
||||||
|
/* Get a 1-4 byte variable-length value and adjust the pointer past it.
|
||||||
|
These are a succession of 7-bit values with a MSB bit of zero marking the end */
|
||||||
|
|
||||||
|
unsigned long val;
|
||||||
|
int i, byte;
|
||||||
|
|
||||||
|
val = 0;
|
||||||
|
for (i = 0; i < 4; ++i) {
|
||||||
|
byte = buffer_byte ((*ptr)++);
|
||||||
|
val = (val << 7) | (byte & 0x7f);
|
||||||
|
if (!(byte & 0x80))
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*************** Process the MIDI track data ***************************/
|
||||||
|
|
||||||
|
/* Skip in the track for the next "note on", "note off" or "set tempo" command,
|
||||||
|
then record that information in the track status block and return. */
|
||||||
|
|
||||||
|
void AudioGeneratorMIDI::find_note (int tracknum) {
|
||||||
|
unsigned long int delta_time;
|
||||||
|
int event, chan;
|
||||||
|
int note, velocity, controller, pressure, pitchbend, instrument;
|
||||||
|
int meta_cmd, meta_length;
|
||||||
|
unsigned long int sysex_length;
|
||||||
|
struct track_status *t;
|
||||||
|
const char *tag;
|
||||||
|
|
||||||
|
/* process events */
|
||||||
|
|
||||||
|
t = &track[tracknum]; /* our track status structure */
|
||||||
|
while (t->trkptr < t->trkend) {
|
||||||
|
|
||||||
|
delta_time = get_varlen (&t->trkptr);
|
||||||
|
t->time += delta_time;
|
||||||
|
if (buffer_byte (t->trkptr) < 0x80)
|
||||||
|
event = t->last_event; /* using "running status": same event as before */
|
||||||
|
else { /* otherwise get new "status" (event type) */
|
||||||
|
event = buffer_byte (t->trkptr++);
|
||||||
|
}
|
||||||
|
if (event == 0xff) { /* meta-event */
|
||||||
|
meta_cmd = buffer_byte (t->trkptr++);
|
||||||
|
meta_length = get_varlen(&t->trkptr);
|
||||||
|
switch (meta_cmd) {
|
||||||
|
case 0x00:
|
||||||
|
break;
|
||||||
|
case 0x01:
|
||||||
|
tag = "description";
|
||||||
|
goto show_text;
|
||||||
|
case 0x02:
|
||||||
|
tag = "copyright";
|
||||||
|
goto show_text;
|
||||||
|
case 0x03:
|
||||||
|
tag = "track name";
|
||||||
|
goto show_text;
|
||||||
|
case 0x04:
|
||||||
|
tag = "instrument name";
|
||||||
|
goto show_text;
|
||||||
|
case 0x05:
|
||||||
|
tag = "lyric";
|
||||||
|
goto show_text;
|
||||||
|
case 0x06:
|
||||||
|
tag = "marked point";
|
||||||
|
goto show_text;
|
||||||
|
case 0x07:
|
||||||
|
tag = "cue point";
|
||||||
|
show_text:
|
||||||
|
break;
|
||||||
|
case 0x20:
|
||||||
|
break;
|
||||||
|
case 0x2f:
|
||||||
|
break;
|
||||||
|
case 0x51: /* tempo: 3 byte big-endian integer! */
|
||||||
|
t->cmd = CMD_TEMPO;
|
||||||
|
t->tempo = rev_long (buffer_int32 (t->trkptr - 1)) & 0xffffffL;
|
||||||
|
t->trkptr += meta_length;
|
||||||
|
return;
|
||||||
|
case 0x54:
|
||||||
|
break;
|
||||||
|
case 0x58:
|
||||||
|
break;
|
||||||
|
case 0x59:
|
||||||
|
break;
|
||||||
|
case 0x7f:
|
||||||
|
tag = "sequencer data";
|
||||||
|
goto show_hex;
|
||||||
|
default: /* unknown meta command */
|
||||||
|
tag = "???";
|
||||||
|
show_hex:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
t->trkptr += meta_length;
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (event < 0x80)
|
||||||
|
midi_error ("Unknown MIDI event type", t->trkptr);
|
||||||
|
|
||||||
|
else {
|
||||||
|
if (event < 0xf0)
|
||||||
|
t->last_event = event; // remember "running status" if not meta or sysex event
|
||||||
|
chan = event & 0xf;
|
||||||
|
t->chan = chan;
|
||||||
|
switch (event >> 4) {
|
||||||
|
case 0x8:
|
||||||
|
t->note = buffer_byte (t->trkptr++);
|
||||||
|
velocity = buffer_byte (t->trkptr++);
|
||||||
|
note_off:
|
||||||
|
t->cmd = CMD_STOPNOTE;
|
||||||
|
return; /* stop processing and return */
|
||||||
|
case 0x9:
|
||||||
|
t->note = buffer_byte (t->trkptr++);
|
||||||
|
velocity = buffer_byte (t->trkptr++);
|
||||||
|
if (velocity == 0) /* some scores use note-on with zero velocity for off! */
|
||||||
|
goto note_off;
|
||||||
|
t->velocity = velocity;
|
||||||
|
t->cmd = CMD_PLAYNOTE;
|
||||||
|
return; /* stop processing and return */
|
||||||
|
case 0xa:
|
||||||
|
note = buffer_byte (t->trkptr++);
|
||||||
|
velocity = buffer_byte (t->trkptr++);
|
||||||
|
break;
|
||||||
|
case 0xb:
|
||||||
|
controller = buffer_byte (t->trkptr++);
|
||||||
|
velocity = buffer_byte (t->trkptr++);
|
||||||
|
break;
|
||||||
|
case 0xc:
|
||||||
|
instrument = buffer_byte (t->trkptr++);
|
||||||
|
midi_chan_instrument[chan] = instrument; // record new instrument for this channel
|
||||||
|
break;
|
||||||
|
case 0xd:
|
||||||
|
pressure = buffer_byte (t->trkptr++);
|
||||||
|
break;
|
||||||
|
case 0xe:
|
||||||
|
pitchbend = buffer_byte (t->trkptr) | (buffer_byte (t->trkptr + 1) << 7);
|
||||||
|
t->trkptr += 2;
|
||||||
|
break;
|
||||||
|
case 0xf:
|
||||||
|
sysex_length = get_varlen (&t->trkptr);
|
||||||
|
t->trkptr += sysex_length;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
midi_error ("Unknown MIDI command", t->trkptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t->cmd = CMD_TRACKDONE; /* no more notes to process */
|
||||||
|
++tracks_done;
|
||||||
|
|
||||||
|
// Remove unused warnings..maybe some day we'll look at these
|
||||||
|
(void)note;
|
||||||
|
(void)controller;
|
||||||
|
(void)pressure;
|
||||||
|
(void)pitchbend;
|
||||||
|
(void)tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Open file, parse headers, get ready tio process MIDI
|
||||||
|
void AudioGeneratorMIDI::PrepareMIDI(AudioFileSource *src)
|
||||||
|
{
|
||||||
|
MakeStreamFromAFS(src, &afsMIDI);
|
||||||
|
tsf_stream_wrap_cached(&afsMIDI, 32, 64, &buffer);
|
||||||
|
buflen = buffer.size (buffer.data);
|
||||||
|
|
||||||
|
/* process the MIDI file header */
|
||||||
|
|
||||||
|
hdrptr = buffer.tell (buffer.data); /* pointer to file and track headers */
|
||||||
|
process_header ();
|
||||||
|
printf (" Processing %d tracks.\n", num_tracks);
|
||||||
|
if (num_tracks > MAX_TRACKS)
|
||||||
|
midi_error ("Too many tracks", buffer.tell (buffer.data));
|
||||||
|
|
||||||
|
/* initialize processing of all the tracks */
|
||||||
|
|
||||||
|
for (tracknum = 0; tracknum < num_tracks; ++tracknum) {
|
||||||
|
start_track (tracknum); /* process the track header */
|
||||||
|
find_note (tracknum); /* position to the first note on/off */
|
||||||
|
}
|
||||||
|
|
||||||
|
notes_skipped = 0;
|
||||||
|
tracknum = 0;
|
||||||
|
earliest_tracknum = 0;
|
||||||
|
earliest_time = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parses the note on/offs until we are ready to render some more samples. Then return the
|
||||||
|
// total number of samples to render before we need to be called again
|
||||||
|
int AudioGeneratorMIDI::PlayMIDI()
|
||||||
|
{
|
||||||
|
/* Continue processing all tracks, in an order based on the simulated time.
|
||||||
|
This is not unlike multiway merging used for tape sorting algoritms in the 50's! */
|
||||||
|
|
||||||
|
do { /* while there are still track notes to process */
|
||||||
|
static struct track_status *trk;
|
||||||
|
static struct tonegen_status *tg;
|
||||||
|
static int tgnum;
|
||||||
|
static int count_tracks;
|
||||||
|
static unsigned long delta_time, delta_msec;
|
||||||
|
|
||||||
|
/* Find the track with the earliest event time,
|
||||||
|
and output a delay command if time has advanced.
|
||||||
|
|
||||||
|
A potential improvement: If there are multiple tracks with the same time,
|
||||||
|
first do the ones with STOPNOTE as the next command, if any. That would
|
||||||
|
help avoid running out of tone generators. In practice, though, most MIDI
|
||||||
|
files do all the STOPNOTEs first anyway, so it won't have much effect.
|
||||||
|
*/
|
||||||
|
|
||||||
|
earliest_time = 0x7fffffff;
|
||||||
|
|
||||||
|
/* Usually we start with the track after the one we did last time (tracknum),
|
||||||
|
so that if we run out of tone generators, we have been fair to all the tracks.
|
||||||
|
The alternate "strategy1" says we always start with track 0, which means
|
||||||
|
that we favor early tracks over later ones when there aren't enough tone generators.
|
||||||
|
*/
|
||||||
|
|
||||||
|
count_tracks = num_tracks;
|
||||||
|
do {
|
||||||
|
if (++tracknum >= num_tracks)
|
||||||
|
tracknum = 0;
|
||||||
|
trk = &track[tracknum];
|
||||||
|
if (trk->cmd != CMD_TRACKDONE && trk->time < earliest_time) {
|
||||||
|
earliest_time = trk->time;
|
||||||
|
earliest_tracknum = tracknum;
|
||||||
|
}
|
||||||
|
} while (--count_tracks);
|
||||||
|
|
||||||
|
tracknum = earliest_tracknum; /* the track we picked */
|
||||||
|
trk = &track[tracknum];
|
||||||
|
if (earliest_time < timenow)
|
||||||
|
midi_error ("INTERNAL: time went backwards", trk->trkptr);
|
||||||
|
|
||||||
|
/* If time has advanced, output a "delay" command */
|
||||||
|
|
||||||
|
delta_time = earliest_time - timenow;
|
||||||
|
if (delta_time) {
|
||||||
|
/* Convert ticks to milliseconds based on the current tempo */
|
||||||
|
unsigned long long temp;
|
||||||
|
temp = ((unsigned long long) delta_time * tempo) / ticks_per_beat;
|
||||||
|
delta_msec = temp / 1000; // get around LCC compiler bug
|
||||||
|
if (delta_msec > 0x7fff)
|
||||||
|
midi_error ("INTERNAL: time delta too big", trk->trkptr);
|
||||||
|
int samples = (((int) delta_msec) * freq) / 1000;
|
||||||
|
timenow = earliest_time;
|
||||||
|
return samples;
|
||||||
|
}
|
||||||
|
timenow = earliest_time;
|
||||||
|
|
||||||
|
/* If this track event is "set tempo", just change the global tempo.
|
||||||
|
That affects how we generate "delay" commands. */
|
||||||
|
|
||||||
|
if (trk->cmd == CMD_TEMPO) {
|
||||||
|
tempo = trk->tempo;
|
||||||
|
find_note (tracknum);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* If this track event is "stop note", process it and all subsequent "stop notes" for this track
|
||||||
|
that are happening at the same time. Doing so frees up as many tone generators as possible. */
|
||||||
|
|
||||||
|
else if (trk->cmd == CMD_STOPNOTE)
|
||||||
|
do {
|
||||||
|
// stop a note
|
||||||
|
for (tgnum = 0; tgnum < num_tonegens; ++tgnum) { /* find which generator is playing it */
|
||||||
|
tg = &tonegen[tgnum];
|
||||||
|
if (tg->playing && tg->track == tracknum && tg->note == trk->note) {
|
||||||
|
tsf_note_off (g_tsf, tg->instrument, tg->note);
|
||||||
|
tg->playing = false;
|
||||||
|
trk->tonegens[tgnum] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
find_note (tracknum); // use up the note
|
||||||
|
} while (trk->cmd == CMD_STOPNOTE && trk->time == timenow);
|
||||||
|
|
||||||
|
/* If this track event is "start note", process only it.
|
||||||
|
Don't do more than one, so we allow other tracks their chance at grabbing tone generators. */
|
||||||
|
|
||||||
|
else if (trk->cmd == CMD_PLAYNOTE) {
|
||||||
|
bool foundgen = false;
|
||||||
|
/* if not, then try for any free tone generator */
|
||||||
|
if (!foundgen)
|
||||||
|
for (tgnum = 0; tgnum < num_tonegens; ++tgnum) {
|
||||||
|
tg = &tonegen[tgnum];
|
||||||
|
if (!tg->playing) {
|
||||||
|
foundgen = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (foundgen) {
|
||||||
|
if (tgnum + 1 > num_tonegens_used)
|
||||||
|
num_tonegens_used = tgnum + 1;
|
||||||
|
tg->playing = true;
|
||||||
|
tg->track = tracknum;
|
||||||
|
tg->note = trk->note;
|
||||||
|
trk->tonegens[tgnum] = true;
|
||||||
|
trk->preferred_tonegen = tgnum;
|
||||||
|
if (tg->instrument != midi_chan_instrument[trk->chan]) { /* new instrument for this generator */
|
||||||
|
tg->instrument = midi_chan_instrument[trk->chan];
|
||||||
|
}
|
||||||
|
tsf_note_on (g_tsf, tg->instrument, tg->note, trk->velocity / 127.0); // velocity = 0...127
|
||||||
|
} else {
|
||||||
|
++notes_skipped;
|
||||||
|
}
|
||||||
|
find_note (tracknum); // use up the note
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (tracks_done < num_tracks);
|
||||||
|
return -1; // EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void AudioGeneratorMIDI::StopMIDI()
|
||||||
|
{
|
||||||
|
|
||||||
|
buffer.close(buffer.data);
|
||||||
|
tsf_close(g_tsf);
|
||||||
|
printf (" %s %d tone generators were used.\n",
|
||||||
|
num_tonegens_used < num_tonegens ? "Only" : "All", num_tonegens_used);
|
||||||
|
if (notes_skipped)
|
||||||
|
printf
|
||||||
|
(" %d notes were skipped because there weren't enough tone generators.\n", notes_skipped);
|
||||||
|
|
||||||
|
printf (" Done.\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool AudioGeneratorMIDI::begin(AudioFileSource *src, AudioOutput *out)
|
||||||
|
{
|
||||||
|
// Clear out status variables
|
||||||
|
for (int i=0; i<MAX_TONEGENS; i++) memset(&tonegen[i], 0, sizeof(struct tonegen_status));
|
||||||
|
for (int i=0; i<MAX_TRACKS; i++) memset(&track[i], 0, sizeof(struct track_status));
|
||||||
|
memset(midi_chan_instrument, 0, sizeof(midi_chan_instrument));
|
||||||
|
|
||||||
|
g_tsf = tsf_load(&afsSF2);
|
||||||
|
if (!g_tsf) return false;
|
||||||
|
tsf_set_output (g_tsf, TSF_MONO, freq, -10 /* dB gain -10 */ );
|
||||||
|
|
||||||
|
if (!out->SetRate( freq )) return false;
|
||||||
|
if (!out->SetBitsPerSample( 16 )) return false;
|
||||||
|
if (!out->SetChannels( 1 )) return false;
|
||||||
|
if (!out->begin()) return false;
|
||||||
|
|
||||||
|
output = out;
|
||||||
|
file = src;
|
||||||
|
|
||||||
|
running = true;
|
||||||
|
|
||||||
|
PrepareMIDI(src);
|
||||||
|
|
||||||
|
samplesToPlay = 0;
|
||||||
|
numSamplesRendered = 0;
|
||||||
|
sentSamplesRendered = 0;
|
||||||
|
|
||||||
|
sawEOF = false;
|
||||||
|
return running;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool AudioGeneratorMIDI::loop()
|
||||||
|
{
|
||||||
|
static int c = 0;
|
||||||
|
|
||||||
|
if (!running) goto done; // Nothing to do here!
|
||||||
|
|
||||||
|
// First, try and push in the stored sample. If we can't, then punt and try later
|
||||||
|
if (!output->ConsumeSample(lastSample)) goto done; // Can't send, but no error detected
|
||||||
|
|
||||||
|
// Try and stuff the buffer one sample at a time
|
||||||
|
do {
|
||||||
|
c++;
|
||||||
|
if (c%44100 == 0) yield();
|
||||||
|
|
||||||
|
play:
|
||||||
|
|
||||||
|
if (sentSamplesRendered < numSamplesRendered) {
|
||||||
|
lastSample[AudioOutput::LEFTCHANNEL] = samplesRendered[sentSamplesRendered];
|
||||||
|
lastSample[AudioOutput::RIGHTCHANNEL] = samplesRendered[sentSamplesRendered];
|
||||||
|
sentSamplesRendered++;
|
||||||
|
} else if (samplesToPlay) {
|
||||||
|
numSamplesRendered = sizeof(samplesRendered)/sizeof(samplesRendered[0]);
|
||||||
|
if ((int)samplesToPlay < (int)(sizeof(samplesRendered)/sizeof(samplesRendered[0]))) numSamplesRendered = samplesToPlay;
|
||||||
|
tsf_render_short_fast(g_tsf, samplesRendered, numSamplesRendered, 0);
|
||||||
|
lastSample[AudioOutput::LEFTCHANNEL] = samplesRendered[0];
|
||||||
|
lastSample[AudioOutput::RIGHTCHANNEL] = samplesRendered[0];
|
||||||
|
sentSamplesRendered = 1;
|
||||||
|
samplesToPlay -= numSamplesRendered;
|
||||||
|
} else {
|
||||||
|
numSamplesRendered = 0;
|
||||||
|
sentSamplesRendered = 0;
|
||||||
|
if (sawEOF) {
|
||||||
|
running = false;
|
||||||
|
} else {
|
||||||
|
samplesToPlay = PlayMIDI();
|
||||||
|
if (samplesToPlay == -1) {
|
||||||
|
sawEOF = true;
|
||||||
|
samplesToPlay = freq / 2;
|
||||||
|
}
|
||||||
|
goto play;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (running && output->ConsumeSample(lastSample));
|
||||||
|
|
||||||
|
done:
|
||||||
|
file->loop();
|
||||||
|
output->loop();
|
||||||
|
|
||||||
|
return running;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioGeneratorMIDI::stop()
|
||||||
|
{
|
||||||
|
StopMIDI();
|
||||||
|
output->stop();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int AudioGeneratorMIDI::afs_read(void *data, void *ptr, unsigned int size)
|
||||||
|
{
|
||||||
|
AudioFileSource *s = reinterpret_cast<AudioFileSource *>(data);
|
||||||
|
return s->read(ptr, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
int AudioGeneratorMIDI::afs_tell(void *data)
|
||||||
|
{
|
||||||
|
AudioFileSource *s = reinterpret_cast<AudioFileSource *>(data);
|
||||||
|
return s->getPos();
|
||||||
|
}
|
||||||
|
|
||||||
|
int AudioGeneratorMIDI::afs_skip(void *data, unsigned int count)
|
||||||
|
{
|
||||||
|
AudioFileSource *s = reinterpret_cast<AudioFileSource *>(data);
|
||||||
|
return s->seek(count, SEEK_CUR);
|
||||||
|
}
|
||||||
|
|
||||||
|
int AudioGeneratorMIDI::afs_seek(void *data, unsigned int pos)
|
||||||
|
{
|
||||||
|
AudioFileSource *s = reinterpret_cast<AudioFileSource *>(data);
|
||||||
|
return s->seek(pos, SEEK_SET);
|
||||||
|
}
|
||||||
|
|
||||||
|
int AudioGeneratorMIDI::afs_close(void *data)
|
||||||
|
{
|
||||||
|
AudioFileSource *s = reinterpret_cast<AudioFileSource *>(data);
|
||||||
|
return s->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
int AudioGeneratorMIDI::afs_size(void *data)
|
||||||
|
{
|
||||||
|
AudioFileSource *s = reinterpret_cast<AudioFileSource *>(data);
|
||||||
|
return s->getSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioGeneratorMIDI::MakeStreamFromAFS(AudioFileSource *src, tsf_stream *afs)
|
||||||
|
{
|
||||||
|
afs->data = reinterpret_cast<void*>(src);
|
||||||
|
afs->read = &afs_read;
|
||||||
|
afs->tell = &afs_tell;
|
||||||
|
afs->skip = &afs_skip;
|
||||||
|
afs->seek = &afs_seek;
|
||||||
|
afs->close = &afs_close;
|
||||||
|
afs->size = &afs_size;
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,181 @@
|
||||||
|
/*
|
||||||
|
AudioGeneratorMIDI
|
||||||
|
Audio output generator that plays MIDI files using a SF2 SoundFont
|
||||||
|
|
||||||
|
Copyright (C) 2017 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _AUDIOGENERATORMIDI_H
|
||||||
|
#define _AUDIOGENERATORMIDI_H
|
||||||
|
|
||||||
|
#include "AudioGenerator.h"
|
||||||
|
|
||||||
|
#define TSF_NO_STDIO
|
||||||
|
#include "libtinysoundfont/tsf.h"
|
||||||
|
|
||||||
|
class AudioGeneratorMIDI : public AudioGenerator
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
AudioGeneratorMIDI() { freq=44100; running = false; };
|
||||||
|
virtual ~AudioGeneratorMIDI() override {};
|
||||||
|
bool SetSoundfont(AudioFileSource *newsf2) {
|
||||||
|
if (isRunning()) return false;
|
||||||
|
sf2 = newsf2;
|
||||||
|
MakeStreamFromAFS(sf2, &afsSF2);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
bool SetSampleRate(int newfreq) {
|
||||||
|
if (isRunning()) return false;
|
||||||
|
freq = newfreq;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
virtual bool begin(AudioFileSource *mid, AudioOutput *output) override;
|
||||||
|
virtual bool loop() override;
|
||||||
|
virtual bool stop() override;
|
||||||
|
virtual bool isRunning() override { return running; };
|
||||||
|
|
||||||
|
private:
|
||||||
|
int freq;
|
||||||
|
tsf *g_tsf;
|
||||||
|
struct tsf_stream buffer;
|
||||||
|
struct tsf_stream afsMIDI;
|
||||||
|
struct tsf_stream afsSF2;
|
||||||
|
AudioFileSource *sf2;
|
||||||
|
AudioFileSource *midi;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
struct midi_header {
|
||||||
|
int8_t MThd[4];
|
||||||
|
uint32_t header_size;
|
||||||
|
uint16_t format_type;
|
||||||
|
uint16_t number_of_tracks;
|
||||||
|
uint16_t time_division;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct track_header {
|
||||||
|
int8_t MTrk[4];
|
||||||
|
uint32_t track_size;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum { MAX_TONEGENS = 32, /* max tone generators: tones we can play simultaneously */
|
||||||
|
MAX_TRACKS = 24
|
||||||
|
}; /* max number of MIDI tracks we will process */
|
||||||
|
|
||||||
|
int hdrptr;
|
||||||
|
unsigned long buflen;
|
||||||
|
int num_tracks;
|
||||||
|
int tracks_done = 0;
|
||||||
|
int num_tonegens = MAX_TONEGENS;
|
||||||
|
int num_tonegens_used = 0;
|
||||||
|
unsigned int ticks_per_beat = 240;
|
||||||
|
unsigned long timenow = 0;
|
||||||
|
unsigned long tempo; /* current tempo in usec/qnote */
|
||||||
|
// State needed for PlayMID()
|
||||||
|
int notes_skipped = 0;
|
||||||
|
int tracknum = 0;
|
||||||
|
int earliest_tracknum = 0;
|
||||||
|
unsigned long earliest_time = 0;
|
||||||
|
|
||||||
|
struct tonegen_status { /* current status of a tone generator */
|
||||||
|
bool playing; /* is it playing? */
|
||||||
|
char track; /* if so, which track is the note from? */
|
||||||
|
char note; /* what note is playing? */
|
||||||
|
char instrument; /* what instrument? */
|
||||||
|
} tonegen[MAX_TONEGENS];
|
||||||
|
|
||||||
|
struct track_status { /* current processing point of a MIDI track */
|
||||||
|
int trkptr; /* ptr to the next note change */
|
||||||
|
int trkend; /* ptr past the end of the track */
|
||||||
|
unsigned long time; /* what time we're at in the score */
|
||||||
|
unsigned long tempo; /* the tempo last set, in usec per qnote */
|
||||||
|
unsigned int preferred_tonegen; /* for strategy2, try to use this generator */
|
||||||
|
unsigned char cmd; /* CMD_xxxx next to do */
|
||||||
|
unsigned char note; /* for which note */
|
||||||
|
unsigned char chan; /* from which channel it was */
|
||||||
|
unsigned char velocity; /* the current volume */
|
||||||
|
unsigned char last_event; /* the last event, for MIDI's "running status" */
|
||||||
|
bool tonegens[MAX_TONEGENS]; /* which tone generators our notes are playing on */
|
||||||
|
} track[MAX_TRACKS];
|
||||||
|
|
||||||
|
int midi_chan_instrument[16]; /* which instrument is currently being played on each channel */
|
||||||
|
|
||||||
|
/* output bytestream commands, which are also stored in track_status.cmd */
|
||||||
|
enum { CMD_PLAYNOTE = 0x90, /* play a note: low nibble is generator #, note is next byte */
|
||||||
|
CMD_STOPNOTE = 0x80, /* stop a note: low nibble is generator # */
|
||||||
|
CMD_INSTRUMENT = 0xc0, /* change instrument; low nibble is generator #, instrument is next byte */
|
||||||
|
CMD_RESTART = 0xe0, /* restart the score from the beginning */
|
||||||
|
CMD_STOP = 0xf0, /* stop playing */
|
||||||
|
CMD_TEMPO = 0xFE, /* tempo in usec per quarter note ("beat") */
|
||||||
|
CMD_TRACKDONE = 0xFF
|
||||||
|
}; /* no more data left in this track */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* portable string length */
|
||||||
|
int strlength (const char *str) {
|
||||||
|
int i;
|
||||||
|
for (i = 0; str[i] != '\0'; ++i);
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* match a constant character sequence */
|
||||||
|
|
||||||
|
int charcmp (const char *buf, const char *match) {
|
||||||
|
int len, i;
|
||||||
|
len = strlength (match);
|
||||||
|
for (i = 0; i < len; ++i)
|
||||||
|
if (buf[i] != match[i])
|
||||||
|
return 0;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned char buffer_byte (int offset);
|
||||||
|
unsigned short buffer_short (int offset);
|
||||||
|
unsigned int buffer_int32 (int offset);
|
||||||
|
|
||||||
|
void midi_error (const char *msg, int curpos);
|
||||||
|
void chk_bufdata (int ptr, unsigned long int len);
|
||||||
|
uint16_t rev_short (uint16_t val);
|
||||||
|
uint32_t rev_long (uint32_t val);
|
||||||
|
void process_header (void);
|
||||||
|
void start_track (int tracknum);
|
||||||
|
|
||||||
|
unsigned long get_varlen (int *ptr);
|
||||||
|
void find_note (int tracknum);
|
||||||
|
void PrepareMIDI(AudioFileSource *src);
|
||||||
|
int PlayMIDI();
|
||||||
|
void StopMIDI();
|
||||||
|
|
||||||
|
// tsf_stream <-> AudioFileSource
|
||||||
|
static int afs_read(void *data, void *ptr, unsigned int size);
|
||||||
|
static int afs_tell(void *data);
|
||||||
|
static int afs_skip(void *data, unsigned int count);
|
||||||
|
static int afs_seek(void *data, unsigned int pos);
|
||||||
|
static int afs_close(void *data);
|
||||||
|
static int afs_size(void *data);
|
||||||
|
void MakeStreamFromAFS(AudioFileSource *src, tsf_stream *afs);
|
||||||
|
|
||||||
|
int samplesToPlay;
|
||||||
|
bool sawEOF;
|
||||||
|
int numSamplesRendered;
|
||||||
|
int sentSamplesRendered ;
|
||||||
|
short samplesRendered[256];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
|
@ -0,0 +1,876 @@
|
||||||
|
/*
|
||||||
|
AudioGeneratorMOD
|
||||||
|
Audio output generator that plays Amiga MOD tracker files
|
||||||
|
|
||||||
|
Copyright (C) 2017 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
#define PGM_READ_UNALIGNED 0
|
||||||
|
|
||||||
|
#include "AudioGeneratorMOD.h"
|
||||||
|
|
||||||
|
/*
|
||||||
|
Ported/hacked out from STELLARPLAYER by Ronen K.
|
||||||
|
http://mobile4dev.blogspot.com/2012/11/stellaris-launchpad-mod-player.html
|
||||||
|
A version exists in GitHub at https://github.com/steveway/stellarplayer
|
||||||
|
and also at https://github.com/MikesModz/StellarPlayer
|
||||||
|
Both which were themselves a port of the PIC32 MOD player
|
||||||
|
https://www.youtube.com/watch?v=i3Yl0TISQBE (seems to no longer be available.)
|
||||||
|
|
||||||
|
Most changes involved reducing memory usage by changing data structures,
|
||||||
|
moving constants to PROGMEM and minor tweaks to allow non pow2 buffer sizes.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma GCC optimize ("O3")
|
||||||
|
|
||||||
|
#define NOTE(r, c) (Player.currentPattern.note8[r][c]==NONOTE8?NONOTE:8*Player.currentPattern.note8[r][c])
|
||||||
|
|
||||||
|
#ifndef min
|
||||||
|
#define min(X,Y) ((X) < (Y) ? (X) : (Y))
|
||||||
|
#endif
|
||||||
|
|
||||||
|
AudioGeneratorMOD::AudioGeneratorMOD()
|
||||||
|
{
|
||||||
|
sampleRate = 44100;
|
||||||
|
fatBufferSize = 6 * 1024;
|
||||||
|
stereoSeparation = 32;
|
||||||
|
mixerTick = 0;
|
||||||
|
usePAL = false;
|
||||||
|
UpdateAmiga();
|
||||||
|
running = false;
|
||||||
|
file = NULL;
|
||||||
|
output = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioGeneratorMOD::~AudioGeneratorMOD()
|
||||||
|
{
|
||||||
|
// Free any remaining buffers
|
||||||
|
for (int i = 0; i < CHANNELS; i++) {
|
||||||
|
FatBuffer.channels[i] = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioGeneratorMOD::stop()
|
||||||
|
{
|
||||||
|
// We may be stopping because of allocation failures, so always deallocate
|
||||||
|
for (int i = 0; i < CHANNELS; i++) {
|
||||||
|
free(FatBuffer.channels[i]);
|
||||||
|
FatBuffer.channels[i] = NULL;
|
||||||
|
}
|
||||||
|
if (file) file->close();
|
||||||
|
running = false;
|
||||||
|
output->stop();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioGeneratorMOD::loop()
|
||||||
|
{
|
||||||
|
if (!running) goto done; // Easy-peasy
|
||||||
|
|
||||||
|
// First, try and push in the stored sample. If we can't, then punt and try later
|
||||||
|
if (!output->ConsumeSample(lastSample)) goto done; // FIFO full, wait...
|
||||||
|
|
||||||
|
// Now advance enough times to fill the i2s buffer
|
||||||
|
do {
|
||||||
|
if (mixerTick == 0) {
|
||||||
|
running = RunPlayer();
|
||||||
|
if (!running) {
|
||||||
|
stop();
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
mixerTick = Player.samplesPerTick;
|
||||||
|
}
|
||||||
|
GetSample( lastSample );
|
||||||
|
mixerTick--;
|
||||||
|
} while (output->ConsumeSample(lastSample));
|
||||||
|
|
||||||
|
done:
|
||||||
|
file->loop();
|
||||||
|
output->loop();
|
||||||
|
|
||||||
|
// We'll be left with one sample still in our buffer because it couldn't fit in the FIFO
|
||||||
|
return running;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioGeneratorMOD::begin(AudioFileSource *source, AudioOutput *out)
|
||||||
|
{
|
||||||
|
if (running) stop();
|
||||||
|
|
||||||
|
if (!source) return false;
|
||||||
|
file = source;
|
||||||
|
if (!out) return false;
|
||||||
|
output = out;
|
||||||
|
|
||||||
|
if (!file->isOpen()) return false; // Can't read the file!
|
||||||
|
|
||||||
|
// Set the output values properly
|
||||||
|
if (!output->SetRate(sampleRate)) return false;
|
||||||
|
if (!output->SetBitsPerSample(16)) return false;
|
||||||
|
if (!output->SetChannels(2)) return false;
|
||||||
|
if (!output->begin()) return false;
|
||||||
|
|
||||||
|
UpdateAmiga();
|
||||||
|
|
||||||
|
for (int i = 0; i < CHANNELS; i++) {
|
||||||
|
FatBuffer.channels[i] = reinterpret_cast<uint8_t*>(malloc(fatBufferSize));
|
||||||
|
if (!FatBuffer.channels[i]) {
|
||||||
|
stop();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!LoadMOD()) {
|
||||||
|
stop();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
running = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sorted Amiga periods
|
||||||
|
static const uint16_t amigaPeriods[296] PROGMEM = {
|
||||||
|
907, 900, 894, 887, 881, 875, 868, 862, // -8 to -1
|
||||||
|
856, 850, 844, 838, 832, 826, 820, 814, // C-1 to +7
|
||||||
|
808, 802, 796, 791, 785, 779, 774, 768, // C#1 to +7
|
||||||
|
762, 757, 752, 746, 741, 736, 730, 725, // D-1 to +7
|
||||||
|
720, 715, 709, 704, 699, 694, 689, 684, // D#1 to +7
|
||||||
|
678, 675, 670, 665, 660, 655, 651, 646, // E-1 to +7
|
||||||
|
640, 636, 632, 628, 623, 619, 614, 610, // F-1 to +7
|
||||||
|
604, 601, 597, 592, 588, 584, 580, 575, // F#1 to +7
|
||||||
|
570, 567, 563, 559, 555, 551, 547, 543, // G-1 to +7
|
||||||
|
538, 535, 532, 528, 524, 520, 516, 513, // G#1 to +7
|
||||||
|
508, 505, 502, 498, 494, 491, 487, 484, // A-1 to +7
|
||||||
|
480, 477, 474, 470, 467, 463, 460, 457, // A#1 to +7
|
||||||
|
453, 450, 447, 444, 441, 437, 434, 431, // B-1 to +7
|
||||||
|
428, 425, 422, 419, 416, 413, 410, 407, // C-2 to +7
|
||||||
|
404, 401, 398, 395, 392, 390, 387, 384, // C#2 to +7
|
||||||
|
381, 379, 376, 373, 370, 368, 365, 363, // D-2 to +7
|
||||||
|
360, 357, 355, 352, 350, 347, 345, 342, // D#2 to +7
|
||||||
|
339, 337, 335, 332, 330, 328, 325, 323, // E-2 to +7
|
||||||
|
320, 318, 316, 314, 312, 309, 307, 305, // F-2 to +7
|
||||||
|
302, 300, 298, 296, 294, 292, 290, 288, // F#2 to +7
|
||||||
|
285, 284, 282, 280, 278, 276, 274, 272, // G-2 to +7
|
||||||
|
269, 268, 266, 264, 262, 260, 258, 256, // G#2 to +7
|
||||||
|
254, 253, 251, 249, 247, 245, 244, 242, // A-2 to +7
|
||||||
|
240, 238, 237, 235, 233, 232, 230, 228, // A#2 to +7
|
||||||
|
226, 225, 223, 222, 220, 219, 217, 216, // B-2 to +7
|
||||||
|
214, 212, 211, 209, 208, 206, 205, 203, // C-3 to +7
|
||||||
|
202, 200, 199, 198, 196, 195, 193, 192, // C#3 to +7
|
||||||
|
190, 189, 188, 187, 185, 184, 183, 181, // D-3 to +7
|
||||||
|
180, 179, 177, 176, 175, 174, 172, 171, // D#3 to +7
|
||||||
|
170, 169, 167, 166, 165, 164, 163, 161, // E-3 to +7
|
||||||
|
160, 159, 158, 157, 156, 155, 154, 152, // F-3 to +7
|
||||||
|
151, 150, 149, 148, 147, 146, 145, 144, // F#3 to +7
|
||||||
|
143, 142, 141, 140, 139, 138, 137, 136, // G-3 to +7
|
||||||
|
135, 134, 133, 132, 131, 130, 129, 128, // G#3 to +7
|
||||||
|
127, 126, 125, 125, 123, 123, 122, 121, // A-3 to +7
|
||||||
|
120, 119, 118, 118, 117, 116, 115, 114, // A#3 to +7
|
||||||
|
113, 113, 112, 111, 110, 109, 109, 108 // B-3 to +7
|
||||||
|
};
|
||||||
|
#define ReadAmigaPeriods(a) (uint16_t)pgm_read_word(amigaPeriods + (a))
|
||||||
|
|
||||||
|
static const uint8_t sine[64] PROGMEM = {
|
||||||
|
0, 24, 49, 74, 97, 120, 141, 161,
|
||||||
|
180, 197, 212, 224, 235, 244, 250, 253,
|
||||||
|
255, 253, 250, 244, 235, 224, 212, 197,
|
||||||
|
180, 161, 141, 120, 97, 74, 49, 24
|
||||||
|
};
|
||||||
|
#define ReadSine(a) pgm_read_byte(sine + (a))
|
||||||
|
|
||||||
|
|
||||||
|
static inline uint16_t MakeWord(uint8_t h, uint8_t l) { return h << 8 | l; }
|
||||||
|
|
||||||
|
bool AudioGeneratorMOD::LoadHeader()
|
||||||
|
{
|
||||||
|
uint8_t i;
|
||||||
|
uint8_t temp[4];
|
||||||
|
uint8_t junk[22];
|
||||||
|
|
||||||
|
if (20 != file->read(/*Mod.name*/junk, 20)) return false; // Skip MOD name
|
||||||
|
for (i = 0; i < SAMPLES; i++) {
|
||||||
|
if (22 != file->read(junk /*Mod.samples[i].name*/, 22)) return false; // Skip sample name
|
||||||
|
if (2 != file->read(temp, 2)) return false;
|
||||||
|
Mod.samples[i].length = MakeWord(temp[0], temp[1]) * 2;
|
||||||
|
if (1 != file->read(reinterpret_cast<uint8_t*>(&Mod.samples[i].fineTune), 1)) return false;
|
||||||
|
if (Mod.samples[i].fineTune > 7) Mod.samples[i].fineTune -= 16;
|
||||||
|
if (1 != file->read(&Mod.samples[i].volume, 1)) return false;
|
||||||
|
if (2 != file->read(temp, 2)) return false;
|
||||||
|
Mod.samples[i].loopBegin = MakeWord(temp[0], temp[1]) * 2;
|
||||||
|
if (2 != file->read(temp, 2)) return false;
|
||||||
|
Mod.samples[i].loopLength = MakeWord(temp[0], temp[1]) * 2;
|
||||||
|
if (Mod.samples[i].loopBegin + Mod.samples[i].loopLength > Mod.samples[i].length)
|
||||||
|
Mod.samples[i].loopLength = Mod.samples[i].length - Mod.samples[i].loopBegin;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (1 != file->read(&Mod.songLength, 1)) return false;
|
||||||
|
if (1 != file->read(temp, 1)) return false; // Discard this byte
|
||||||
|
|
||||||
|
Mod.numberOfPatterns = 0;
|
||||||
|
for (i = 0; i < 128; i++) {
|
||||||
|
if (1 != file->read(&Mod.order[i], 1)) return false;
|
||||||
|
if (Mod.order[i] > Mod.numberOfPatterns)
|
||||||
|
Mod.numberOfPatterns = Mod.order[i];
|
||||||
|
}
|
||||||
|
Mod.numberOfPatterns++;
|
||||||
|
|
||||||
|
// Offset 1080
|
||||||
|
if (4 != file->read(temp, 4)) return false;;
|
||||||
|
if (!strncmp(reinterpret_cast<const char*>(temp + 1), "CHN", 3))
|
||||||
|
Mod.numberOfChannels = temp[0] - '0';
|
||||||
|
else if (!strncmp(reinterpret_cast<const char*>(temp + 2), "CH", 2))
|
||||||
|
Mod.numberOfChannels = (temp[0] - '0') * 10 + temp[1] - '0';
|
||||||
|
else
|
||||||
|
Mod.numberOfChannels = 4;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioGeneratorMOD::LoadSamples()
|
||||||
|
{
|
||||||
|
uint8_t i;
|
||||||
|
uint32_t fileOffset = 1084 + Mod.numberOfPatterns * ROWS * Mod.numberOfChannels * 4 - 1;
|
||||||
|
|
||||||
|
for (i = 0; i < SAMPLES; i++) {
|
||||||
|
|
||||||
|
if (Mod.samples[i].length) {
|
||||||
|
Mixer.sampleBegin[i] = fileOffset;
|
||||||
|
Mixer.sampleEnd[i] = fileOffset + Mod.samples[i].length;
|
||||||
|
if (Mod.samples[i].loopLength > 2) {
|
||||||
|
Mixer.sampleloopBegin[i] = fileOffset + Mod.samples[i].loopBegin;
|
||||||
|
Mixer.sampleLoopLength[i] = Mod.samples[i].loopLength;
|
||||||
|
Mixer.sampleLoopEnd[i] = Mixer.sampleloopBegin[i] + Mixer.sampleLoopLength[i];
|
||||||
|
} else {
|
||||||
|
Mixer.sampleloopBegin[i] = 0;
|
||||||
|
Mixer.sampleLoopLength[i] = 0;
|
||||||
|
Mixer.sampleLoopEnd[i] = 0;
|
||||||
|
}
|
||||||
|
fileOffset += Mod.samples[i].length;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioGeneratorMOD::LoadPattern(uint8_t pattern)
|
||||||
|
{
|
||||||
|
uint8_t row;
|
||||||
|
uint8_t channel;
|
||||||
|
uint8_t i;
|
||||||
|
uint8_t temp[4];
|
||||||
|
uint16_t amigaPeriod;
|
||||||
|
|
||||||
|
if (!file->seek(1084 + pattern * ROWS * Mod.numberOfChannels * 4, SEEK_SET)) return false;
|
||||||
|
|
||||||
|
for (row = 0; row < ROWS; row++) {
|
||||||
|
for (channel = 0; channel < Mod.numberOfChannels; channel++) {
|
||||||
|
|
||||||
|
if (4 != file->read(temp, 4)) return false;
|
||||||
|
|
||||||
|
Player.currentPattern.sampleNumber[row][channel] = (temp[0] & 0xF0) + (temp[2] >> 4);
|
||||||
|
|
||||||
|
amigaPeriod = ((temp[0] & 0xF) << 8) + temp[1];
|
||||||
|
// Player.currentPattern.note[row][channel] = NONOTE;
|
||||||
|
Player.currentPattern.note8[row][channel] = NONOTE8;
|
||||||
|
for (i = 1; i < 37; i++)
|
||||||
|
if (amigaPeriod > ReadAmigaPeriods(i * 8) - 3 &&
|
||||||
|
amigaPeriod < ReadAmigaPeriods(i * 8) + 3)
|
||||||
|
Player.currentPattern.note8[row][channel] = i;
|
||||||
|
|
||||||
|
Player.currentPattern.effectNumber[row][channel] = temp[2] & 0xF;
|
||||||
|
Player.currentPattern.effectParameter[row][channel] = temp[3];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioGeneratorMOD::Portamento(uint8_t channel)
|
||||||
|
{
|
||||||
|
if (Player.lastAmigaPeriod[channel] < Player.portamentoNote[channel]) {
|
||||||
|
Player.lastAmigaPeriod[channel] += Player.portamentoSpeed[channel];
|
||||||
|
if (Player.lastAmigaPeriod[channel] > Player.portamentoNote[channel])
|
||||||
|
Player.lastAmigaPeriod[channel] = Player.portamentoNote[channel];
|
||||||
|
}
|
||||||
|
if (Player.lastAmigaPeriod[channel] > Player.portamentoNote[channel]) {
|
||||||
|
Player.lastAmigaPeriod[channel] -= Player.portamentoSpeed[channel];
|
||||||
|
if (Player.lastAmigaPeriod[channel] < Player.portamentoNote[channel])
|
||||||
|
Player.lastAmigaPeriod[channel] = Player.portamentoNote[channel];
|
||||||
|
}
|
||||||
|
Mixer.channelFrequency[channel] = Player.amiga / Player.lastAmigaPeriod[channel];
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioGeneratorMOD::Vibrato(uint8_t channel)
|
||||||
|
{
|
||||||
|
uint16_t delta;
|
||||||
|
uint16_t temp;
|
||||||
|
|
||||||
|
temp = Player.vibratoPos[channel] & 31;
|
||||||
|
|
||||||
|
switch (Player.waveControl[channel] & 3) {
|
||||||
|
case 0:
|
||||||
|
delta = ReadSine(temp);
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
temp <<= 3;
|
||||||
|
if (Player.vibratoPos[channel] < 0)
|
||||||
|
temp = 255 - temp;
|
||||||
|
delta = temp;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
delta = 255;
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
delta = rand() & 255;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
delta *= Player.vibratoDepth[channel];
|
||||||
|
delta >>= 7;
|
||||||
|
|
||||||
|
if (Player.vibratoPos[channel] >= 0)
|
||||||
|
Mixer.channelFrequency[channel] = Player.amiga / (Player.lastAmigaPeriod[channel] + delta);
|
||||||
|
else
|
||||||
|
Mixer.channelFrequency[channel] = Player.amiga / (Player.lastAmigaPeriod[channel] - delta);
|
||||||
|
|
||||||
|
Player.vibratoPos[channel] += Player.vibratoSpeed[channel];
|
||||||
|
if (Player.vibratoPos[channel] > 31) Player.vibratoPos[channel] -= 64;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioGeneratorMOD::Tremolo(uint8_t channel)
|
||||||
|
{
|
||||||
|
uint16_t delta;
|
||||||
|
uint16_t temp;
|
||||||
|
|
||||||
|
temp = Player.tremoloPos[channel] & 31;
|
||||||
|
|
||||||
|
switch (Player.waveControl[channel] & 3) {
|
||||||
|
case 0:
|
||||||
|
delta = ReadSine(temp);
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
temp <<= 3;
|
||||||
|
if (Player.tremoloPos[channel] < 0)
|
||||||
|
temp = 255 - temp;
|
||||||
|
delta = temp;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
delta = 255;
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
delta = rand() & 255;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
delta *= Player.tremoloDepth[channel];
|
||||||
|
delta >>= 6;
|
||||||
|
|
||||||
|
if (Player.tremoloPos[channel] >= 0) {
|
||||||
|
if (Player.volume[channel] + delta > 64) delta = 64 - Player.volume[channel];
|
||||||
|
Mixer.channelVolume[channel] = Player.volume[channel] + delta;
|
||||||
|
} else {
|
||||||
|
if (Player.volume[channel] - delta < 0) delta = Player.volume[channel];
|
||||||
|
Mixer.channelVolume[channel] = Player.volume[channel] - delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
Player.tremoloPos[channel] += Player.tremoloSpeed[channel];
|
||||||
|
if (Player.tremoloPos[channel] > 31) Player.tremoloPos[channel] -= 64;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioGeneratorMOD::ProcessRow()
|
||||||
|
{
|
||||||
|
bool jumpFlag;
|
||||||
|
bool breakFlag;
|
||||||
|
uint8_t channel;
|
||||||
|
uint8_t sampleNumber;
|
||||||
|
uint16_t note;
|
||||||
|
uint8_t effectNumber;
|
||||||
|
uint8_t effectParameter;
|
||||||
|
uint8_t effectParameterX;
|
||||||
|
uint8_t effectParameterY;
|
||||||
|
uint16_t sampleOffset;
|
||||||
|
|
||||||
|
if (!running) return false;
|
||||||
|
|
||||||
|
Player.lastRow = Player.row++;
|
||||||
|
jumpFlag = false;
|
||||||
|
breakFlag = false;
|
||||||
|
for (channel = 0; channel < Mod.numberOfChannels; channel++) {
|
||||||
|
|
||||||
|
sampleNumber = Player.currentPattern.sampleNumber[Player.lastRow][channel];
|
||||||
|
note = NOTE(Player.lastRow, channel);
|
||||||
|
effectNumber = Player.currentPattern.effectNumber[Player.lastRow][channel];
|
||||||
|
effectParameter = Player.currentPattern.effectParameter[Player.lastRow][channel];
|
||||||
|
effectParameterX = effectParameter >> 4;
|
||||||
|
effectParameterY = effectParameter & 0xF;
|
||||||
|
sampleOffset = 0;
|
||||||
|
|
||||||
|
if (sampleNumber) {
|
||||||
|
Player.lastSampleNumber[channel] = sampleNumber - 1;
|
||||||
|
if (!(effectParameter == 0xE && effectParameterX == NOTEDELAY))
|
||||||
|
Player.volume[channel] = Mod.samples[Player.lastSampleNumber[channel]].volume;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (note != NONOTE) {
|
||||||
|
Player.lastNote[channel] = note;
|
||||||
|
Player.amigaPeriod[channel] = ReadAmigaPeriods(note + Mod.samples[Player.lastSampleNumber[channel]].fineTune);
|
||||||
|
|
||||||
|
if (effectNumber != TONEPORTAMENTO && effectNumber != PORTAMENTOVOLUMESLIDE)
|
||||||
|
Player.lastAmigaPeriod[channel] = Player.amigaPeriod[channel];
|
||||||
|
|
||||||
|
if (!(Player.waveControl[channel] & 0x80)) Player.vibratoPos[channel] = 0;
|
||||||
|
if (!(Player.waveControl[channel] & 0x08)) Player.tremoloPos[channel] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (effectNumber) {
|
||||||
|
case TONEPORTAMENTO:
|
||||||
|
if (effectParameter) Player.portamentoSpeed[channel] = effectParameter;
|
||||||
|
Player.portamentoNote[channel] = Player.amigaPeriod[channel];
|
||||||
|
note = NONOTE;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case VIBRATO:
|
||||||
|
if (effectParameterX) Player.vibratoSpeed[channel] = effectParameterX;
|
||||||
|
if (effectParameterY) Player.vibratoDepth[channel] = effectParameterY;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PORTAMENTOVOLUMESLIDE:
|
||||||
|
Player.portamentoNote[channel] = Player.amigaPeriod[channel];
|
||||||
|
note = NONOTE;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TREMOLO:
|
||||||
|
if (effectParameterX) Player.tremoloSpeed[channel] = effectParameterX;
|
||||||
|
if (effectParameterY) Player.tremoloDepth[channel] = effectParameterY;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SETCHANNELPANNING:
|
||||||
|
Mixer.channelPanning[channel] = effectParameter >> 1;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SETSAMPLEOFFSET:
|
||||||
|
sampleOffset = effectParameter << 8;
|
||||||
|
if (sampleOffset > Mod.samples[Player.lastSampleNumber[channel]].length)
|
||||||
|
sampleOffset = Mod.samples[Player.lastSampleNumber[channel]].length;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case JUMPTOORDER:
|
||||||
|
Player.orderIndex = effectParameter;
|
||||||
|
if (Player.orderIndex >= Mod.songLength)
|
||||||
|
Player.orderIndex = 0;
|
||||||
|
Player.row = 0;
|
||||||
|
jumpFlag = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SETVOLUME:
|
||||||
|
if (effectParameter > 64) Player.volume[channel] = 64;
|
||||||
|
else Player.volume[channel] = effectParameter;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case BREAKPATTERNTOROW:
|
||||||
|
Player.row = effectParameterX * 10 + effectParameterY;
|
||||||
|
if (Player.row >= ROWS)
|
||||||
|
Player.row = 0;
|
||||||
|
if (!jumpFlag && !breakFlag) {
|
||||||
|
Player.orderIndex++;
|
||||||
|
if (Player.orderIndex >= Mod.songLength)
|
||||||
|
Player.orderIndex = 0;
|
||||||
|
}
|
||||||
|
breakFlag = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 0xE:
|
||||||
|
switch (effectParameterX) {
|
||||||
|
case FINEPORTAMENTOUP:
|
||||||
|
Player.lastAmigaPeriod[channel] -= effectParameterY;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FINEPORTAMENTODOWN:
|
||||||
|
Player.lastAmigaPeriod[channel] += effectParameterY;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SETVIBRATOWAVEFORM:
|
||||||
|
Player.waveControl[channel] &= 0xF0;
|
||||||
|
Player.waveControl[channel] |= effectParameterY;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SETFINETUNE:
|
||||||
|
Mod.samples[Player.lastSampleNumber[channel]].fineTune = effectParameterY;
|
||||||
|
if (Mod.samples[Player.lastSampleNumber[channel]].fineTune > 7)
|
||||||
|
Mod.samples[Player.lastSampleNumber[channel]].fineTune -= 16;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PATTERNLOOP:
|
||||||
|
if (effectParameterY) {
|
||||||
|
if (Player.patternLoopCount[channel])
|
||||||
|
Player.patternLoopCount[channel]--;
|
||||||
|
else
|
||||||
|
Player.patternLoopCount[channel] = effectParameterY;
|
||||||
|
if (Player.patternLoopCount[channel])
|
||||||
|
Player.row = Player.patternLoopRow[channel] - 1;
|
||||||
|
} else
|
||||||
|
Player.patternLoopRow[channel] = Player.row;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SETTREMOLOWAVEFORM:
|
||||||
|
Player.waveControl[channel] &= 0xF;
|
||||||
|
Player.waveControl[channel] |= effectParameterY << 4;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FINEVOLUMESLIDEUP:
|
||||||
|
Player.volume[channel] += effectParameterY;
|
||||||
|
if (Player.volume[channel] > 64) Player.volume[channel] = 64;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FINEVOLUMESLIDEDOWN:
|
||||||
|
Player.volume[channel] -= effectParameterY;
|
||||||
|
if (Player.volume[channel] < 0) Player.volume[channel] = 0;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case NOTECUT:
|
||||||
|
note = NONOTE;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PATTERNDELAY:
|
||||||
|
Player.patternDelay = effectParameterY;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case INVERTLOOP:
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SETSPEED:
|
||||||
|
if (effectParameter < 0x20)
|
||||||
|
Player.speed = effectParameter;
|
||||||
|
else
|
||||||
|
Player.samplesPerTick = sampleRate / (2 * effectParameter / 5);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (note != NONOTE || (Player.lastAmigaPeriod[channel] &&
|
||||||
|
effectNumber != VIBRATO && effectNumber != VIBRATOVOLUMESLIDE &&
|
||||||
|
!(effectNumber == 0xE && effectParameterX == NOTEDELAY)))
|
||||||
|
Mixer.channelFrequency[channel] = Player.amiga / Player.lastAmigaPeriod[channel];
|
||||||
|
|
||||||
|
if (note != NONOTE)
|
||||||
|
Mixer.channelSampleOffset[channel] = sampleOffset << DIVIDER;
|
||||||
|
|
||||||
|
if (sampleNumber)
|
||||||
|
Mixer.channelSampleNumber[channel] = Player.lastSampleNumber[channel];
|
||||||
|
|
||||||
|
if (effectNumber != TREMOLO)
|
||||||
|
Mixer.channelVolume[channel] = Player.volume[channel];
|
||||||
|
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioGeneratorMOD::ProcessTick()
|
||||||
|
{
|
||||||
|
uint8_t channel;
|
||||||
|
uint8_t sampleNumber;
|
||||||
|
uint16_t note;
|
||||||
|
uint8_t effectNumber;
|
||||||
|
uint8_t effectParameter;
|
||||||
|
uint8_t effectParameterX;
|
||||||
|
uint8_t effectParameterY;
|
||||||
|
uint16_t tempNote;
|
||||||
|
|
||||||
|
if (!running) return false;
|
||||||
|
|
||||||
|
for (channel = 0; channel < Mod.numberOfChannels; channel++) {
|
||||||
|
|
||||||
|
if (Player.lastAmigaPeriod[channel]) {
|
||||||
|
|
||||||
|
sampleNumber = Player.currentPattern.sampleNumber[Player.lastRow][channel];
|
||||||
|
// note = Player.currentPattern.note[Player.lastRow][channel];
|
||||||
|
note = NOTE(Player.lastRow, channel);
|
||||||
|
effectNumber = Player.currentPattern.effectNumber[Player.lastRow][channel];
|
||||||
|
effectParameter = Player.currentPattern.effectParameter[Player.lastRow][channel];
|
||||||
|
effectParameterX = effectParameter >> 4;
|
||||||
|
effectParameterY = effectParameter & 0xF;
|
||||||
|
|
||||||
|
switch (effectNumber) {
|
||||||
|
case ARPEGGIO:
|
||||||
|
if (effectParameter)
|
||||||
|
switch (Player.tick % 3) {
|
||||||
|
case 0:
|
||||||
|
Mixer.channelFrequency[channel] = Player.amiga / Player.lastAmigaPeriod[channel];
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
tempNote = Player.lastNote[channel] + effectParameterX * 8 + Mod.samples[Player.lastSampleNumber[channel]].fineTune;
|
||||||
|
if (tempNote < 296) Mixer.channelFrequency[channel] = Player.amiga / ReadAmigaPeriods(tempNote);
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
tempNote = Player.lastNote[channel] + effectParameterY * 8 + Mod.samples[Player.lastSampleNumber[channel]].fineTune;
|
||||||
|
if (tempNote < 296) Mixer.channelFrequency[channel] = Player.amiga / ReadAmigaPeriods(tempNote);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PORTAMENTOUP:
|
||||||
|
Player.lastAmigaPeriod[channel] -= effectParameter;
|
||||||
|
if (Player.lastAmigaPeriod[channel] < 113) Player.lastAmigaPeriod[channel] = 113;
|
||||||
|
Mixer.channelFrequency[channel] = Player.amiga / Player.lastAmigaPeriod[channel];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PORTAMENTODOWN:
|
||||||
|
Player.lastAmigaPeriod[channel] += effectParameter;
|
||||||
|
if (Player.lastAmigaPeriod[channel] > 856) Player.lastAmigaPeriod[channel] = 856;
|
||||||
|
Mixer.channelFrequency[channel] = Player.amiga / Player.lastAmigaPeriod[channel];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TONEPORTAMENTO:
|
||||||
|
Portamento(channel);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case VIBRATO:
|
||||||
|
Vibrato(channel);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PORTAMENTOVOLUMESLIDE:
|
||||||
|
Portamento(channel);
|
||||||
|
Player.volume[channel] += effectParameterX - effectParameterY;
|
||||||
|
if (Player.volume[channel] < 0) Player.volume[channel] = 0;
|
||||||
|
else if (Player.volume[channel] > 64) Player.volume[channel] = 64;
|
||||||
|
Mixer.channelVolume[channel] = Player.volume[channel];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case VIBRATOVOLUMESLIDE:
|
||||||
|
Vibrato(channel);
|
||||||
|
Player.volume[channel] += effectParameterX - effectParameterY;
|
||||||
|
if (Player.volume[channel] < 0) Player.volume[channel] = 0;
|
||||||
|
else if (Player.volume[channel] > 64) Player.volume[channel] = 64;
|
||||||
|
Mixer.channelVolume[channel] = Player.volume[channel];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TREMOLO:
|
||||||
|
Tremolo(channel);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case VOLUMESLIDE:
|
||||||
|
Player.volume[channel] += effectParameterX - effectParameterY;
|
||||||
|
if (Player.volume[channel] < 0) Player.volume[channel] = 0;
|
||||||
|
else if (Player.volume[channel] > 64) Player.volume[channel] = 64;
|
||||||
|
Mixer.channelVolume[channel] = Player.volume[channel];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 0xE:
|
||||||
|
switch (effectParameterX) {
|
||||||
|
case RETRIGGERNOTE:
|
||||||
|
if (!effectParameterY) break;
|
||||||
|
if (!(Player.tick % effectParameterY)) {
|
||||||
|
Mixer.channelSampleOffset[channel] = 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case NOTECUT:
|
||||||
|
if (Player.tick == effectParameterY)
|
||||||
|
Mixer.channelVolume[channel] = Player.volume[channel] = 0;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case NOTEDELAY:
|
||||||
|
if (Player.tick == effectParameterY) {
|
||||||
|
if (sampleNumber) Player.volume[channel] = Mod.samples[Player.lastSampleNumber[channel]].volume;
|
||||||
|
if (note != NONOTE) Mixer.channelSampleOffset[channel] = 0;
|
||||||
|
Mixer.channelFrequency[channel] = Player.amiga / Player.lastAmigaPeriod[channel];
|
||||||
|
Mixer.channelVolume[channel] = Player.volume[channel];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioGeneratorMOD::RunPlayer()
|
||||||
|
{
|
||||||
|
if (!running) return false;
|
||||||
|
|
||||||
|
if (Player.tick == Player.speed) {
|
||||||
|
Player.tick = 0;
|
||||||
|
|
||||||
|
if (Player.row == ROWS) {
|
||||||
|
Player.orderIndex++;
|
||||||
|
if (Player.orderIndex == Mod.songLength)
|
||||||
|
{
|
||||||
|
//Player.orderIndex = 0;
|
||||||
|
// No loop, just say we're done!
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Player.row = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Player.patternDelay) {
|
||||||
|
Player.patternDelay--;
|
||||||
|
} else {
|
||||||
|
if (Player.orderIndex != Player.oldOrderIndex)
|
||||||
|
if (!LoadPattern(Mod.order[Player.orderIndex])) return false;
|
||||||
|
Player.oldOrderIndex = Player.orderIndex;
|
||||||
|
if (!ProcessRow()) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
if (!ProcessTick()) return false;
|
||||||
|
}
|
||||||
|
Player.tick++;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioGeneratorMOD::GetSample(int16_t sample[2])
|
||||||
|
{
|
||||||
|
int16_t sumL;
|
||||||
|
int16_t sumR;
|
||||||
|
uint8_t channel;
|
||||||
|
uint32_t samplePointer;
|
||||||
|
int8_t current;
|
||||||
|
int8_t next;
|
||||||
|
int16_t out;
|
||||||
|
|
||||||
|
if (!running) return;
|
||||||
|
|
||||||
|
sumL = 0;
|
||||||
|
sumR = 0;
|
||||||
|
for (channel = 0; channel < Mod.numberOfChannels; channel++) {
|
||||||
|
|
||||||
|
if (!Mixer.channelFrequency[channel] ||
|
||||||
|
!Mod.samples[Mixer.channelSampleNumber[channel]].length) continue;
|
||||||
|
|
||||||
|
Mixer.channelSampleOffset[channel] += Mixer.channelFrequency[channel];
|
||||||
|
|
||||||
|
if (!Mixer.channelVolume[channel]) continue;
|
||||||
|
|
||||||
|
samplePointer = Mixer.sampleBegin[Mixer.channelSampleNumber[channel]] +
|
||||||
|
(Mixer.channelSampleOffset[channel] >> DIVIDER);
|
||||||
|
|
||||||
|
if (Mixer.sampleLoopLength[Mixer.channelSampleNumber[channel]]) {
|
||||||
|
|
||||||
|
if (samplePointer >= Mixer.sampleLoopEnd[Mixer.channelSampleNumber[channel]]) {
|
||||||
|
Mixer.channelSampleOffset[channel] -= Mixer.sampleLoopLength[Mixer.channelSampleNumber[channel]] << DIVIDER;
|
||||||
|
samplePointer -= Mixer.sampleLoopLength[Mixer.channelSampleNumber[channel]];
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
if (samplePointer >= Mixer.sampleEnd[Mixer.channelSampleNumber[channel]]) {
|
||||||
|
Mixer.channelFrequency[channel] = 0;
|
||||||
|
samplePointer = Mixer.sampleEnd[Mixer.channelSampleNumber[channel]];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (samplePointer < FatBuffer.samplePointer[channel] ||
|
||||||
|
samplePointer >= FatBuffer.samplePointer[channel] + fatBufferSize - 1 ||
|
||||||
|
Mixer.channelSampleNumber[channel] != FatBuffer.channelSampleNumber[channel]) {
|
||||||
|
|
||||||
|
uint16_t toRead = Mixer.sampleEnd[Mixer.channelSampleNumber[channel]] - samplePointer + 1;
|
||||||
|
if (toRead > fatBufferSize) toRead = fatBufferSize;
|
||||||
|
|
||||||
|
if (!file->seek(samplePointer, SEEK_SET)) {
|
||||||
|
stop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (toRead != file->read(FatBuffer.channels[channel], toRead)) {
|
||||||
|
stop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FatBuffer.samplePointer[channel] = samplePointer;
|
||||||
|
FatBuffer.channelSampleNumber[channel] = Mixer.channelSampleNumber[channel];
|
||||||
|
}
|
||||||
|
|
||||||
|
current = FatBuffer.channels[channel][(samplePointer - FatBuffer.samplePointer[channel]) /*& (FATBUFFERSIZE - 1)*/];
|
||||||
|
next = FatBuffer.channels[channel][(samplePointer + 1 - FatBuffer.samplePointer[channel]) /*& (FATBUFFERSIZE - 1)*/];
|
||||||
|
|
||||||
|
out = current;
|
||||||
|
|
||||||
|
// Integer linear interpolation
|
||||||
|
out += (next - current) * (Mixer.channelSampleOffset[channel] & ((1 << DIVIDER) - 1)) >> DIVIDER;
|
||||||
|
|
||||||
|
// Upscale to BITDEPTH
|
||||||
|
out <<= BITDEPTH - 8;
|
||||||
|
|
||||||
|
// Channel volume
|
||||||
|
out = out * Mixer.channelVolume[channel] >> 6;
|
||||||
|
|
||||||
|
// Channel panning
|
||||||
|
sumL += out * min(128 - Mixer.channelPanning[channel], 64) >> 6;
|
||||||
|
sumR += out * min(Mixer.channelPanning[channel], 64) >> 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Downscale to BITDEPTH
|
||||||
|
sumL /= Mod.numberOfChannels;
|
||||||
|
sumR /= Mod.numberOfChannels;
|
||||||
|
|
||||||
|
// Fill the sound buffer with unsigned values
|
||||||
|
sample[AudioOutput::LEFTCHANNEL] = sumL + (1 << (BITDEPTH - 1));
|
||||||
|
sample[AudioOutput::RIGHTCHANNEL] = sumR + (1 << (BITDEPTH - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioGeneratorMOD::LoadMOD()
|
||||||
|
{
|
||||||
|
uint8_t channel;
|
||||||
|
|
||||||
|
if (!LoadHeader()) return false;
|
||||||
|
LoadSamples();
|
||||||
|
|
||||||
|
Player.amiga = AMIGA;
|
||||||
|
Player.samplesPerTick = sampleRate / (2 * 125 / 5); // Hz = 2 * BPM / 5
|
||||||
|
Player.speed = 6;
|
||||||
|
Player.tick = Player.speed;
|
||||||
|
Player.row = 0;
|
||||||
|
|
||||||
|
Player.orderIndex = 0;
|
||||||
|
Player.oldOrderIndex = 0xFF;
|
||||||
|
Player.patternDelay = 0;
|
||||||
|
|
||||||
|
for (channel = 0; channel < Mod.numberOfChannels; channel++) {
|
||||||
|
Player.patternLoopCount[channel] = 0;
|
||||||
|
Player.patternLoopRow[channel] = 0;
|
||||||
|
|
||||||
|
Player.lastAmigaPeriod[channel] = 0;
|
||||||
|
|
||||||
|
Player.waveControl[channel] = 0;
|
||||||
|
|
||||||
|
Player.vibratoSpeed[channel] = 0;
|
||||||
|
Player.vibratoDepth[channel] = 0;
|
||||||
|
Player.vibratoPos[channel] = 0;
|
||||||
|
|
||||||
|
Player.tremoloSpeed[channel] = 0;
|
||||||
|
Player.tremoloDepth[channel] = 0;
|
||||||
|
Player.tremoloPos[channel] = 0;
|
||||||
|
|
||||||
|
FatBuffer.samplePointer[channel] = 0;
|
||||||
|
FatBuffer.channelSampleNumber[channel] = 0xFF;
|
||||||
|
|
||||||
|
Mixer.channelSampleOffset[channel] = 0;
|
||||||
|
Mixer.channelFrequency[channel] = 0;
|
||||||
|
Mixer.channelVolume[channel] = 0;
|
||||||
|
switch (channel % 4) {
|
||||||
|
case 0:
|
||||||
|
case 3:
|
||||||
|
Mixer.channelPanning[channel] = stereoSeparation;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Mixer.channelPanning[channel] = 128 - stereoSeparation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,168 @@
|
||||||
|
/*
|
||||||
|
AudioGeneratorMOD
|
||||||
|
Audio output generator that plays Amiga MOD tracker files
|
||||||
|
|
||||||
|
Copyright (C) 2017 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _AUDIOGENERATORMOD_H
|
||||||
|
#define _AUDIOGENERATORMOD_H
|
||||||
|
|
||||||
|
#include "AudioGenerator.h"
|
||||||
|
|
||||||
|
class AudioGeneratorMOD : public AudioGenerator
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
AudioGeneratorMOD();
|
||||||
|
virtual ~AudioGeneratorMOD() override;
|
||||||
|
virtual bool begin(AudioFileSource *source, AudioOutput *output) override;
|
||||||
|
virtual bool loop() override;
|
||||||
|
virtual bool stop() override;
|
||||||
|
virtual bool isRunning() override { return running; }
|
||||||
|
bool SetSampleRate(int hz) { if (running || (hz < 1) || (hz > 96000) ) return false; sampleRate = hz; return true; }
|
||||||
|
bool SetBufferSize(int sz) { if (running || (sz < 1) ) return false; fatBufferSize = sz; return true; }
|
||||||
|
bool SetStereoSeparation(int sep) { if (running || (sep<0) || (sep>64)) return false; stereoSeparation = sep; return true; }
|
||||||
|
bool SetPAL(bool use) { if (running) return false; usePAL = use; return true; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool LoadMOD();
|
||||||
|
bool LoadHeader();
|
||||||
|
void GetSample(int16_t sample[2]);
|
||||||
|
bool RunPlayer();
|
||||||
|
void LoadSamples();
|
||||||
|
bool LoadPattern(uint8_t pattern);
|
||||||
|
bool ProcessTick();
|
||||||
|
bool ProcessRow();
|
||||||
|
void Tremolo(uint8_t channel);
|
||||||
|
void Portamento(uint8_t channel);
|
||||||
|
void Vibrato(uint8_t channel);
|
||||||
|
|
||||||
|
|
||||||
|
protected:
|
||||||
|
int mixerTick;
|
||||||
|
enum {BITDEPTH = 15};
|
||||||
|
int sampleRate;
|
||||||
|
int fatBufferSize; //(6*1024) // File system buffers per-CHANNEL (i.e. total mem required is 4 * FATBUFFERSIZE)
|
||||||
|
enum {DIVIDER = 10}; // Fixed-point mantissa used for integer arithmetic
|
||||||
|
int stereoSeparation; //STEREOSEPARATION = 32; // 0 (max) to 64 (mono)
|
||||||
|
bool usePAL;
|
||||||
|
|
||||||
|
// Hz = 7093789 / (amigaPeriod * 2) for PAL
|
||||||
|
// Hz = 7159091 / (amigaPeriod * 2) for NTSC
|
||||||
|
int AMIGA;
|
||||||
|
void UpdateAmiga() { AMIGA = ((usePAL?7159091:7093789) / 2 / sampleRate << DIVIDER); }
|
||||||
|
|
||||||
|
enum {ROWS = 64, SAMPLES = 31, CHANNELS = 4, NONOTE = 0xFFFF, NONOTE8 = 0xff };
|
||||||
|
|
||||||
|
typedef struct Sample {
|
||||||
|
uint16_t length;
|
||||||
|
int8_t fineTune;
|
||||||
|
uint8_t volume;
|
||||||
|
uint16_t loopBegin;
|
||||||
|
uint16_t loopLength;
|
||||||
|
} Sample;
|
||||||
|
|
||||||
|
typedef struct mod {
|
||||||
|
Sample samples[SAMPLES];
|
||||||
|
uint8_t songLength;
|
||||||
|
uint8_t numberOfPatterns;
|
||||||
|
uint8_t order[128];
|
||||||
|
uint8_t numberOfChannels;
|
||||||
|
} mod;
|
||||||
|
|
||||||
|
// Save 256 bytes by storing raw note values, unpack with macro NOTE
|
||||||
|
typedef struct Pattern {
|
||||||
|
uint8_t sampleNumber[ROWS][CHANNELS];
|
||||||
|
uint8_t note8[ROWS][CHANNELS];
|
||||||
|
uint8_t effectNumber[ROWS][CHANNELS];
|
||||||
|
uint8_t effectParameter[ROWS][CHANNELS];
|
||||||
|
} Pattern;
|
||||||
|
|
||||||
|
typedef struct player {
|
||||||
|
Pattern currentPattern;
|
||||||
|
|
||||||
|
uint32_t amiga;
|
||||||
|
uint16_t samplesPerTick;
|
||||||
|
uint8_t speed;
|
||||||
|
uint8_t tick;
|
||||||
|
uint8_t row;
|
||||||
|
uint8_t lastRow;
|
||||||
|
|
||||||
|
uint8_t orderIndex;
|
||||||
|
uint8_t oldOrderIndex;
|
||||||
|
uint8_t patternDelay;
|
||||||
|
uint8_t patternLoopCount[CHANNELS];
|
||||||
|
uint8_t patternLoopRow[CHANNELS];
|
||||||
|
|
||||||
|
uint8_t lastSampleNumber[CHANNELS];
|
||||||
|
int8_t volume[CHANNELS];
|
||||||
|
uint16_t lastNote[CHANNELS];
|
||||||
|
uint16_t amigaPeriod[CHANNELS];
|
||||||
|
int16_t lastAmigaPeriod[CHANNELS];
|
||||||
|
|
||||||
|
uint16_t portamentoNote[CHANNELS];
|
||||||
|
uint8_t portamentoSpeed[CHANNELS];
|
||||||
|
|
||||||
|
uint8_t waveControl[CHANNELS];
|
||||||
|
|
||||||
|
uint8_t vibratoSpeed[CHANNELS];
|
||||||
|
uint8_t vibratoDepth[CHANNELS];
|
||||||
|
int8_t vibratoPos[CHANNELS];
|
||||||
|
|
||||||
|
uint8_t tremoloSpeed[CHANNELS];
|
||||||
|
uint8_t tremoloDepth[CHANNELS];
|
||||||
|
int8_t tremoloPos[CHANNELS];
|
||||||
|
} player;
|
||||||
|
|
||||||
|
typedef struct mixer {
|
||||||
|
uint32_t sampleBegin[SAMPLES];
|
||||||
|
uint32_t sampleEnd[SAMPLES];
|
||||||
|
uint32_t sampleloopBegin[SAMPLES];
|
||||||
|
uint16_t sampleLoopLength[SAMPLES];
|
||||||
|
uint32_t sampleLoopEnd[SAMPLES];
|
||||||
|
|
||||||
|
uint8_t channelSampleNumber[CHANNELS];
|
||||||
|
uint32_t channelSampleOffset[CHANNELS];
|
||||||
|
uint16_t channelFrequency[CHANNELS];
|
||||||
|
uint8_t channelVolume[CHANNELS];
|
||||||
|
uint8_t channelPanning[CHANNELS];
|
||||||
|
} mixer;
|
||||||
|
|
||||||
|
typedef struct fatBuffer {
|
||||||
|
uint8_t *channels[CHANNELS]; // Make dynamically allocated [FATBUFFERSIZE];
|
||||||
|
uint32_t samplePointer[CHANNELS];
|
||||||
|
uint8_t channelSampleNumber[CHANNELS];
|
||||||
|
} fatBuffer;
|
||||||
|
|
||||||
|
// Effects
|
||||||
|
typedef enum { ARPEGGIO = 0, PORTAMENTOUP, PORTAMENTODOWN, TONEPORTAMENTO, VIBRATO, PORTAMENTOVOLUMESLIDE,
|
||||||
|
VIBRATOVOLUMESLIDE, TREMOLO, SETCHANNELPANNING, SETSAMPLEOFFSET, VOLUMESLIDE, JUMPTOORDER,
|
||||||
|
SETVOLUME, BREAKPATTERNTOROW, ESUBSET, SETSPEED } EffectsValues;
|
||||||
|
|
||||||
|
// 0xE subset
|
||||||
|
typedef enum { SETFILTER = 0, FINEPORTAMENTOUP, FINEPORTAMENTODOWN, GLISSANDOCONTROL, SETVIBRATOWAVEFORM,
|
||||||
|
SETFINETUNE, PATTERNLOOP, SETTREMOLOWAVEFORM, SUBEFFECT8, RETRIGGERNOTE, FINEVOLUMESLIDEUP,
|
||||||
|
FINEVOLUMESLIDEDOWN, NOTECUT, NOTEDELAY, PATTERNDELAY, INVERTLOOP } Effect08Subvalues;
|
||||||
|
|
||||||
|
// Our state lives here...
|
||||||
|
player Player;
|
||||||
|
mod Mod;
|
||||||
|
mixer Mixer;
|
||||||
|
fatBuffer FatBuffer;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
|
@ -0,0 +1,352 @@
|
||||||
|
/*
|
||||||
|
AudioGeneratorMP3
|
||||||
|
Wrap libmad MP3 library to play audio
|
||||||
|
|
||||||
|
Copyright (C) 2017 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
#include "AudioGeneratorMP3.h"
|
||||||
|
|
||||||
|
AudioGeneratorMP3::AudioGeneratorMP3()
|
||||||
|
{
|
||||||
|
running = false;
|
||||||
|
file = NULL;
|
||||||
|
output = NULL;
|
||||||
|
buff = NULL;
|
||||||
|
nsCountMax = 1152/32;
|
||||||
|
madInitted = false;
|
||||||
|
preallocateSpace = NULL;
|
||||||
|
preallocateSize = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioGeneratorMP3::AudioGeneratorMP3(void *space, int size)
|
||||||
|
{
|
||||||
|
running = false;
|
||||||
|
file = NULL;
|
||||||
|
output = NULL;
|
||||||
|
buff = NULL;
|
||||||
|
nsCountMax = 1152/32;
|
||||||
|
madInitted = false;
|
||||||
|
preallocateSpace = space;
|
||||||
|
preallocateSize = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioGeneratorMP3::~AudioGeneratorMP3()
|
||||||
|
{
|
||||||
|
if (!preallocateSpace) {
|
||||||
|
free(buff);
|
||||||
|
free(synth);
|
||||||
|
free(frame);
|
||||||
|
free(stream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool AudioGeneratorMP3::stop()
|
||||||
|
{
|
||||||
|
if (madInitted) {
|
||||||
|
mad_synth_finish(synth);
|
||||||
|
mad_frame_finish(frame);
|
||||||
|
mad_stream_finish(stream);
|
||||||
|
madInitted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!preallocateSpace) {
|
||||||
|
free(buff);
|
||||||
|
free(synth);
|
||||||
|
free(frame);
|
||||||
|
free(stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
buff = NULL;
|
||||||
|
synth = NULL;
|
||||||
|
frame = NULL;
|
||||||
|
stream = NULL;
|
||||||
|
|
||||||
|
running = false;
|
||||||
|
output->stop();
|
||||||
|
return file->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioGeneratorMP3::isRunning()
|
||||||
|
{
|
||||||
|
return running;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum mad_flow AudioGeneratorMP3::ErrorToFlow()
|
||||||
|
{
|
||||||
|
char err[64];
|
||||||
|
char errLine[128];
|
||||||
|
|
||||||
|
// Special case - eat "lost sync @ byte 0" as it always occurs and is not really correct....it never had sync!
|
||||||
|
if ((lastReadPos==0) && (stream->error==MAD_ERROR_LOSTSYNC)) return MAD_FLOW_CONTINUE;
|
||||||
|
|
||||||
|
strcpy_P(err, mad_stream_errorstr(stream));
|
||||||
|
snprintf_P(errLine, sizeof(errLine), PSTR("Decoding error '%s' at byte offset %d"),
|
||||||
|
err, (stream->this_frame - buff) + lastReadPos);
|
||||||
|
yield(); // Something bad happened anyway, ensure WiFi gets some time, too
|
||||||
|
cb.st(stream->error, errLine);
|
||||||
|
return MAD_FLOW_CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum mad_flow AudioGeneratorMP3::Input()
|
||||||
|
{
|
||||||
|
int unused = 0;
|
||||||
|
|
||||||
|
if (stream->next_frame) {
|
||||||
|
unused = lastBuffLen - (stream->next_frame - buff);
|
||||||
|
memmove(buff, stream->next_frame, unused);
|
||||||
|
stream->next_frame = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unused == lastBuffLen) {
|
||||||
|
// Something wicked this way came, throw it all out and try again
|
||||||
|
unused = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastReadPos = file->getPos() - unused;
|
||||||
|
int len = buffLen - unused;
|
||||||
|
len = file->read(buff + unused, len);
|
||||||
|
if ((len == 0) && (unused == 0)) {
|
||||||
|
// Can't read any from the file, and we don't have anything left. It's done....
|
||||||
|
return MAD_FLOW_STOP;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastBuffLen = len + unused;
|
||||||
|
mad_stream_buffer(stream, buff, lastBuffLen);
|
||||||
|
|
||||||
|
return MAD_FLOW_CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool AudioGeneratorMP3::DecodeNextFrame()
|
||||||
|
{
|
||||||
|
if (mad_frame_decode(frame, stream) == -1) {
|
||||||
|
ErrorToFlow(); // Always returns CONTINUE
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
nsCountMax = MAD_NSBSAMPLES(&frame->header);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioGeneratorMP3::GetOneSample(int16_t sample[2])
|
||||||
|
{
|
||||||
|
if (synth->pcm.samplerate != lastRate) {
|
||||||
|
output->SetRate(synth->pcm.samplerate);
|
||||||
|
lastRate = synth->pcm.samplerate;
|
||||||
|
}
|
||||||
|
if (synth->pcm.channels != lastChannels) {
|
||||||
|
output->SetChannels(synth->pcm.channels);
|
||||||
|
lastChannels = synth->pcm.channels;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're here, we have one decoded frame and sent 0 or more samples out
|
||||||
|
if (samplePtr < synth->pcm.length) {
|
||||||
|
sample[AudioOutput::LEFTCHANNEL ] = synth->pcm.samples[0][samplePtr];
|
||||||
|
sample[AudioOutput::RIGHTCHANNEL] = synth->pcm.samples[1][samplePtr];
|
||||||
|
samplePtr++;
|
||||||
|
} else {
|
||||||
|
samplePtr = 0;
|
||||||
|
|
||||||
|
switch ( mad_synth_frame_onens(synth, frame, nsCount++) ) {
|
||||||
|
case MAD_FLOW_STOP:
|
||||||
|
case MAD_FLOW_BREAK: audioLogger->printf_P(PSTR("msf1ns failed\n"));
|
||||||
|
return false; // Either way we're done
|
||||||
|
default:
|
||||||
|
break; // Do nothing
|
||||||
|
}
|
||||||
|
// for IGNORE and CONTINUE, just play what we have now
|
||||||
|
sample[AudioOutput::LEFTCHANNEL ] = synth->pcm.samples[0][samplePtr];
|
||||||
|
sample[AudioOutput::RIGHTCHANNEL] = synth->pcm.samples[1][samplePtr];
|
||||||
|
samplePtr++;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool AudioGeneratorMP3::loop()
|
||||||
|
{
|
||||||
|
if (!running) goto done; // Nothing to do here!
|
||||||
|
|
||||||
|
// First, try and push in the stored sample. If we can't, then punt and try later
|
||||||
|
if (!output->ConsumeSample(lastSample)) goto done; // Can't send, but no error detected
|
||||||
|
|
||||||
|
// Try and stuff the buffer one sample at a time
|
||||||
|
do
|
||||||
|
{
|
||||||
|
// Decode next frame if we're beyond the existing generated data
|
||||||
|
if ( (samplePtr >= synth->pcm.length) && (nsCount >= nsCountMax) ) {
|
||||||
|
retry:
|
||||||
|
if (Input() == MAD_FLOW_STOP) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!DecodeNextFrame()) {
|
||||||
|
goto retry;
|
||||||
|
}
|
||||||
|
samplePtr = 9999;
|
||||||
|
nsCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!GetOneSample(lastSample)) {
|
||||||
|
audioLogger->printf_P(PSTR("G1S failed\n"));
|
||||||
|
running = false;
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
} while (running && output->ConsumeSample(lastSample));
|
||||||
|
|
||||||
|
done:
|
||||||
|
file->loop();
|
||||||
|
output->loop();
|
||||||
|
|
||||||
|
return running;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
bool AudioGeneratorMP3::begin(AudioFileSource *source, AudioOutput *output)
|
||||||
|
{
|
||||||
|
if (!source) return false;
|
||||||
|
file = source;
|
||||||
|
if (!output) return false;
|
||||||
|
this->output = output;
|
||||||
|
if (!file->isOpen()) {
|
||||||
|
audioLogger->printf_P(PSTR("MP3 source file not open\n"));
|
||||||
|
return false; // Error
|
||||||
|
}
|
||||||
|
|
||||||
|
output->SetBitsPerSample(16); // Constant for MP3 decoder
|
||||||
|
output->SetChannels(2);
|
||||||
|
|
||||||
|
if (!output->begin()) return false;
|
||||||
|
|
||||||
|
// Where we are in generating one frame's data, set to invalid so we will run loop on first getsample()
|
||||||
|
samplePtr = 9999;
|
||||||
|
nsCount = 9999;
|
||||||
|
lastRate = 0;
|
||||||
|
lastChannels = 0;
|
||||||
|
lastReadPos = 0;
|
||||||
|
lastBuffLen = 0;
|
||||||
|
|
||||||
|
// Allocate all large memory chunks
|
||||||
|
if (preallocateSpace) {
|
||||||
|
uint8_t *p = reinterpret_cast<uint8_t *>(preallocateSpace);
|
||||||
|
buff = reinterpret_cast<unsigned char *>(p);
|
||||||
|
p += (buffLen+7) & ~7;
|
||||||
|
stream = reinterpret_cast<struct mad_stream *>(p);
|
||||||
|
p += (sizeof(struct mad_stream)+7) & ~7;
|
||||||
|
frame = reinterpret_cast<struct mad_frame *>(p);
|
||||||
|
p += (sizeof(struct mad_frame)+7) & ~7;
|
||||||
|
synth = reinterpret_cast<struct mad_synth *>(p);
|
||||||
|
p += (sizeof(struct mad_synth)+7) & ~7;
|
||||||
|
int neededBytes = p - reinterpret_cast<uint8_t *>(preallocateSpace);
|
||||||
|
if (neededBytes > preallocateSize) {
|
||||||
|
audioLogger->printf_P("OOM error in MP3: Want %d bytes, have %d bytes preallocated.\n", neededBytes, preallocateSize);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
buff = reinterpret_cast<unsigned char *>(malloc(buffLen));
|
||||||
|
stream = reinterpret_cast<struct mad_stream *>(malloc(sizeof(struct mad_stream)));
|
||||||
|
frame = reinterpret_cast<struct mad_frame *>(malloc(sizeof(struct mad_frame)));
|
||||||
|
synth = reinterpret_cast<struct mad_synth *>(malloc(sizeof(struct mad_synth)));
|
||||||
|
if (!buff || !stream || !frame || !synth) {
|
||||||
|
free(buff);
|
||||||
|
free(stream);
|
||||||
|
free(frame);
|
||||||
|
free(synth);
|
||||||
|
buff = NULL;
|
||||||
|
stream = NULL;
|
||||||
|
frame = NULL;
|
||||||
|
synth = NULL;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mad_stream_init(stream);
|
||||||
|
mad_frame_init(frame);
|
||||||
|
mad_synth_init(synth);
|
||||||
|
synth->pcm.length = 0;
|
||||||
|
mad_stream_options(stream, 0); // TODO - add options support
|
||||||
|
madInitted = true;
|
||||||
|
|
||||||
|
running = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The following are helper routines for use in libmad to check stack/heap free
|
||||||
|
// and to determine if there's enough stack space to allocate some blocks there
|
||||||
|
// instead of precious heap.
|
||||||
|
|
||||||
|
#undef stack
|
||||||
|
extern "C" {
|
||||||
|
#ifdef ESP32
|
||||||
|
//TODO - add ESP32 checks
|
||||||
|
void stack(const char *s, const char *t, int i)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
int stackfree()
|
||||||
|
{
|
||||||
|
return 8192;
|
||||||
|
}
|
||||||
|
#elif defined(ESP8266)
|
||||||
|
#include <cont.h>
|
||||||
|
extern cont_t g_cont;
|
||||||
|
|
||||||
|
void stack(const char *s, const char *t, int i)
|
||||||
|
{
|
||||||
|
(void) t;
|
||||||
|
(void) i;
|
||||||
|
register uint32_t *sp asm("a1");
|
||||||
|
int freestack = 4 * (sp - g_cont.stack);
|
||||||
|
int freeheap = ESP.getFreeHeap();
|
||||||
|
if ((freestack < 512) || (freeheap < 5120)) {
|
||||||
|
static int laststack, lastheap;
|
||||||
|
if (laststack!=freestack|| lastheap !=freeheap) {
|
||||||
|
audioLogger->printf_P(PSTR("%s: FREESTACK=%d, FREEHEAP=%d\n"), s, /*t, i,*/ freestack, /*cont_get_free_stack(&g_cont),*/ freeheap);
|
||||||
|
}
|
||||||
|
if (freestack < 256) {
|
||||||
|
audioLogger->printf_P(PSTR("out of stack!\n"));
|
||||||
|
}
|
||||||
|
if (freeheap < 1024) {
|
||||||
|
audioLogger->printf_P(PSTR("out of heap!\n"));
|
||||||
|
}
|
||||||
|
Serial.flush();
|
||||||
|
laststack = freestack;
|
||||||
|
lastheap = freeheap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int stackfree()
|
||||||
|
{
|
||||||
|
register uint32_t *sp asm("a1");
|
||||||
|
int freestack = 4 * (sp - g_cont.stack);
|
||||||
|
return freestack;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
void stack(const char *s, const char *t, int i)
|
||||||
|
{
|
||||||
|
(void) s;
|
||||||
|
(void) t;
|
||||||
|
(void) i;
|
||||||
|
}
|
||||||
|
int stackfree()
|
||||||
|
{
|
||||||
|
return 8192;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
/*
|
||||||
|
AudioGeneratorMP3
|
||||||
|
Wrap libmad MP3 library to play audio
|
||||||
|
|
||||||
|
Copyright (C) 2017 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _AUDIOGENERATORMP3_H
|
||||||
|
#define _AUDIOGENERATORMP3_H
|
||||||
|
|
||||||
|
#include "AudioGenerator.h"
|
||||||
|
#include "libmad/config.h"
|
||||||
|
#include "libmad/mad.h"
|
||||||
|
|
||||||
|
class AudioGeneratorMP3 : public AudioGenerator
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
AudioGeneratorMP3();
|
||||||
|
AudioGeneratorMP3(void *preallocateSpace, int preallocateSize);
|
||||||
|
virtual ~AudioGeneratorMP3() override;
|
||||||
|
virtual bool begin(AudioFileSource *source, AudioOutput *output) override;
|
||||||
|
virtual bool loop() override;
|
||||||
|
virtual bool stop() override;
|
||||||
|
virtual bool isRunning() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void *preallocateSpace;
|
||||||
|
int preallocateSize;
|
||||||
|
|
||||||
|
const int buffLen = 0x600; // Slightly larger than largest MP3 frame
|
||||||
|
unsigned char *buff;
|
||||||
|
int lastReadPos;
|
||||||
|
int lastBuffLen;
|
||||||
|
unsigned int lastRate;
|
||||||
|
int lastChannels;
|
||||||
|
|
||||||
|
// Decoding bits
|
||||||
|
bool madInitted;
|
||||||
|
struct mad_stream *stream;
|
||||||
|
struct mad_frame *frame;
|
||||||
|
struct mad_synth *synth;
|
||||||
|
int samplePtr;
|
||||||
|
int nsCount;
|
||||||
|
int nsCountMax;
|
||||||
|
|
||||||
|
// The internal helpers
|
||||||
|
enum mad_flow ErrorToFlow();
|
||||||
|
enum mad_flow Input();
|
||||||
|
bool DecodeNextFrame();
|
||||||
|
bool GetOneSample(int16_t sample[2]);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
|
@ -0,0 +1,162 @@
|
||||||
|
/*
|
||||||
|
AudioGeneratorMP3
|
||||||
|
Audio output generator using the Helix MP3 decoder
|
||||||
|
|
||||||
|
Copyright (C) 2017 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma GCC optimize ("O3")
|
||||||
|
|
||||||
|
#include "AudioGeneratorMP3a.h"
|
||||||
|
|
||||||
|
|
||||||
|
AudioGeneratorMP3a::AudioGeneratorMP3a()
|
||||||
|
{
|
||||||
|
running = false;
|
||||||
|
file = NULL;
|
||||||
|
output = NULL;
|
||||||
|
hMP3Decoder = MP3InitDecoder();
|
||||||
|
if (!hMP3Decoder) {
|
||||||
|
audioLogger->printf_P(PSTR("Out of memory error! hMP3Decoder==NULL\n"));
|
||||||
|
Serial.flush();
|
||||||
|
}
|
||||||
|
// For sanity's sake...
|
||||||
|
memset(buff, 0, sizeof(buff));
|
||||||
|
memset(outSample, 0, sizeof(outSample));
|
||||||
|
buffValid = 0;
|
||||||
|
lastFrameEnd = 0;
|
||||||
|
validSamples = 0;
|
||||||
|
curSample = 0;
|
||||||
|
lastRate = 0;
|
||||||
|
lastChannels = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioGeneratorMP3a::~AudioGeneratorMP3a()
|
||||||
|
{
|
||||||
|
MP3FreeDecoder(hMP3Decoder);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioGeneratorMP3a::stop()
|
||||||
|
{
|
||||||
|
if (!running) return true;
|
||||||
|
running = false;
|
||||||
|
output->stop();
|
||||||
|
return file->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioGeneratorMP3a::isRunning()
|
||||||
|
{
|
||||||
|
return running;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioGeneratorMP3a::FillBufferWithValidFrame()
|
||||||
|
{
|
||||||
|
buff[0] = 0; // Destroy any existing sync word @ 0
|
||||||
|
int nextSync;
|
||||||
|
do {
|
||||||
|
nextSync = MP3FindSyncWord(buff + lastFrameEnd, buffValid - lastFrameEnd);
|
||||||
|
if (nextSync >= 0) nextSync += lastFrameEnd;
|
||||||
|
lastFrameEnd = 0;
|
||||||
|
if (nextSync == -1) {
|
||||||
|
if (buff[buffValid-1]==0xff) { // Could be 1st half of syncword, preserve it...
|
||||||
|
buff[0] = 0xff;
|
||||||
|
buffValid = file->read(buff+1, sizeof(buff)-1);
|
||||||
|
if (buffValid==0) return false; // No data available, EOF
|
||||||
|
} else { // Try a whole new buffer
|
||||||
|
buffValid = file->read(buff, sizeof(buff));
|
||||||
|
if (buffValid==0) return false; // No data available, EOF
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (nextSync == -1);
|
||||||
|
|
||||||
|
// Move the frame to start at offset 0 in the buffer
|
||||||
|
buffValid -= nextSync; // Throw out prior to nextSync
|
||||||
|
memmove(buff, buff+nextSync, buffValid);
|
||||||
|
|
||||||
|
// We have a sync word at 0 now, try and fill remainder of buffer
|
||||||
|
buffValid += file->read(buff + buffValid, sizeof(buff) - buffValid);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioGeneratorMP3a::loop()
|
||||||
|
{
|
||||||
|
if (!running) goto done; // Nothing to do here!
|
||||||
|
|
||||||
|
// If we've got data, try and pump it out...
|
||||||
|
while (validSamples) {
|
||||||
|
lastSample[0] = outSample[curSample*2];
|
||||||
|
lastSample[1] = outSample[curSample*2 + 1];
|
||||||
|
if (!output->ConsumeSample(lastSample)) goto done; // Can't send, but no error detected
|
||||||
|
validSamples--;
|
||||||
|
curSample++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No samples available, need to decode a new frame
|
||||||
|
if (FillBufferWithValidFrame()) {
|
||||||
|
// buff[0] start of frame, decode it...
|
||||||
|
unsigned char *inBuff = reinterpret_cast<unsigned char *>(buff);
|
||||||
|
int bytesLeft = buffValid;
|
||||||
|
int ret = MP3Decode(hMP3Decoder, &inBuff, &bytesLeft, outSample, 0);
|
||||||
|
if (ret) {
|
||||||
|
// Error, skip the frame...
|
||||||
|
char buff[48];
|
||||||
|
sprintf(buff, "MP3 decode error %d", ret);
|
||||||
|
cb.st(ret, buff);
|
||||||
|
} else {
|
||||||
|
lastFrameEnd = buffValid - bytesLeft;
|
||||||
|
MP3FrameInfo fi;
|
||||||
|
MP3GetLastFrameInfo(hMP3Decoder, &fi);
|
||||||
|
if ((int)fi.samprate!= (int)lastRate) {
|
||||||
|
output->SetRate(fi.samprate);
|
||||||
|
lastRate = fi.samprate;
|
||||||
|
}
|
||||||
|
if (fi.nChans != lastChannels) {
|
||||||
|
output->SetChannels(fi.nChans);
|
||||||
|
lastChannels = fi.nChans;
|
||||||
|
}
|
||||||
|
curSample = 0;
|
||||||
|
validSamples = fi.outputSamps / lastChannels;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
running = false; // No more data, we're done here...
|
||||||
|
}
|
||||||
|
|
||||||
|
done:
|
||||||
|
file->loop();
|
||||||
|
output->loop();
|
||||||
|
|
||||||
|
return running;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioGeneratorMP3a::begin(AudioFileSource *source, AudioOutput *output)
|
||||||
|
{
|
||||||
|
if (!source) return false;
|
||||||
|
file = source;
|
||||||
|
if (!output) return false;
|
||||||
|
this->output = output;
|
||||||
|
if (!file->isOpen()) return false; // Error
|
||||||
|
|
||||||
|
output->begin();
|
||||||
|
|
||||||
|
// AAC always comes out at 16 bits
|
||||||
|
output->SetBitsPerSample(16);
|
||||||
|
|
||||||
|
running = true;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
/*
|
||||||
|
AudioGeneratorMP3
|
||||||
|
Audio output generator using the Helix MP3 decoder
|
||||||
|
|
||||||
|
Copyright (C) 2017 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _AUDIOGENERATORMP3A_H
|
||||||
|
#define _AUDIOGENERATORMP3A_H
|
||||||
|
|
||||||
|
#include "AudioGenerator.h"
|
||||||
|
#include "libhelix-mp3/mp3dec.h"
|
||||||
|
|
||||||
|
class AudioGeneratorMP3a : public AudioGenerator
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
AudioGeneratorMP3a();
|
||||||
|
virtual ~AudioGeneratorMP3a() override;
|
||||||
|
virtual bool begin(AudioFileSource *source, AudioOutput *output) override;
|
||||||
|
virtual bool loop() override;
|
||||||
|
virtual bool stop() override;
|
||||||
|
virtual bool isRunning() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// Helix MP3 decoder
|
||||||
|
HMP3Decoder hMP3Decoder;
|
||||||
|
|
||||||
|
// Input buffering
|
||||||
|
uint8_t buff[1600]; // File buffer required to store at least a whole compressed frame
|
||||||
|
int16_t buffValid;
|
||||||
|
int16_t lastFrameEnd;
|
||||||
|
bool FillBufferWithValidFrame(); // Read until we get a valid syncword and min(feof, 2048) butes in the buffer
|
||||||
|
|
||||||
|
// Output buffering
|
||||||
|
int16_t outSample[1152 * 2]; // Interleaved L/R
|
||||||
|
int16_t validSamples;
|
||||||
|
int16_t curSample;
|
||||||
|
|
||||||
|
// Each frame may change this if they're very strange, I guess
|
||||||
|
unsigned int lastRate;
|
||||||
|
int lastChannels;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
|
@ -0,0 +1,142 @@
|
||||||
|
/*
|
||||||
|
AudioGeneratorOpus
|
||||||
|
Audio output generator that plays Opus audio files
|
||||||
|
|
||||||
|
Copyright (C) 2020 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <AudioGeneratorOpus.h>
|
||||||
|
|
||||||
|
AudioGeneratorOpus::AudioGeneratorOpus()
|
||||||
|
{
|
||||||
|
of = nullptr;
|
||||||
|
buff = nullptr;
|
||||||
|
buffPtr = 0;
|
||||||
|
buffLen = 0;
|
||||||
|
running = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioGeneratorOpus::~AudioGeneratorOpus()
|
||||||
|
{
|
||||||
|
if (of) op_free(of);
|
||||||
|
of = nullptr;
|
||||||
|
free(buff);
|
||||||
|
buff = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
#define OPUS_BUFF 1024
|
||||||
|
|
||||||
|
bool AudioGeneratorOpus::begin(AudioFileSource *source, AudioOutput *output)
|
||||||
|
{
|
||||||
|
buff = (int16_t*)malloc(OPUS_BUFF * sizeof(int16_t));
|
||||||
|
if (!buff) return false;
|
||||||
|
|
||||||
|
if (!source) return false;
|
||||||
|
file = source;
|
||||||
|
if (!output) return false;
|
||||||
|
this->output = output;
|
||||||
|
if (!file->isOpen()) return false; // Error
|
||||||
|
|
||||||
|
of = op_open_callbacks((void*)this, &cb, nullptr, 0, nullptr);
|
||||||
|
if (!of) return false;
|
||||||
|
|
||||||
|
prev_li = -1;
|
||||||
|
lastSample[0] = 0;
|
||||||
|
lastSample[1] = 0;
|
||||||
|
|
||||||
|
buffPtr = 0;
|
||||||
|
buffLen = 0;
|
||||||
|
|
||||||
|
output->begin();
|
||||||
|
|
||||||
|
// These are fixed by Opus
|
||||||
|
output->SetRate(48000);
|
||||||
|
output->SetBitsPerSample(16);
|
||||||
|
output->SetChannels(2);
|
||||||
|
|
||||||
|
running = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioGeneratorOpus::loop()
|
||||||
|
{
|
||||||
|
|
||||||
|
if (!running) goto done;
|
||||||
|
|
||||||
|
if (!output->ConsumeSample(lastSample)) goto done; // Try and send last buffered sample
|
||||||
|
|
||||||
|
do {
|
||||||
|
if (buffPtr == buffLen) {
|
||||||
|
int ret = op_read_stereo(of, (opus_int16 *)buff, OPUS_BUFF);
|
||||||
|
if (ret == OP_HOLE) {
|
||||||
|
// fprintf(stderr,"\nHole detected! Corrupt file segment?\n");
|
||||||
|
continue;
|
||||||
|
} else if (ret <= 0) {
|
||||||
|
running = false;
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
buffPtr = 0;
|
||||||
|
buffLen = ret * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastSample[AudioOutput::LEFTCHANNEL] = buff[buffPtr] & 0xffff;
|
||||||
|
lastSample[AudioOutput::RIGHTCHANNEL] = buff[buffPtr+1] & 0xffff;
|
||||||
|
buffPtr += 2;
|
||||||
|
} while (running && output->ConsumeSample(lastSample));
|
||||||
|
|
||||||
|
done:
|
||||||
|
file->loop();
|
||||||
|
output->loop();
|
||||||
|
|
||||||
|
return running;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioGeneratorOpus::stop()
|
||||||
|
{
|
||||||
|
if (of) op_free(of);
|
||||||
|
of = nullptr;
|
||||||
|
free(buff);
|
||||||
|
buff = nullptr;
|
||||||
|
running = false;
|
||||||
|
output->stop();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioGeneratorOpus::isRunning()
|
||||||
|
{
|
||||||
|
return running;
|
||||||
|
}
|
||||||
|
|
||||||
|
int AudioGeneratorOpus::read_cb(unsigned char *_ptr, int _nbytes) {
|
||||||
|
if (_nbytes == 0) return 0;
|
||||||
|
_nbytes = file->read(_ptr, _nbytes);
|
||||||
|
if (_nbytes == 0) return -1;
|
||||||
|
return _nbytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
int AudioGeneratorOpus::seek_cb(opus_int64 _offset, int _whence) {
|
||||||
|
if (!file->seek((int32_t)_offset, _whence)) return -1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
opus_int64 AudioGeneratorOpus::tell_cb() {
|
||||||
|
return file->getPos();
|
||||||
|
}
|
||||||
|
|
||||||
|
int AudioGeneratorOpus::close_cb() {
|
||||||
|
// NO OP, we close in main loop
|
||||||
|
return 0;
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
/*
|
||||||
|
AudioGeneratorOpus
|
||||||
|
Audio output generator that plays Opus audio files
|
||||||
|
|
||||||
|
Copyright (C) 2020 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _AUDIOGENERATOROPUS_H
|
||||||
|
#define _AUDIOGENERATOROPUS_H
|
||||||
|
|
||||||
|
#include <AudioGenerator.h>
|
||||||
|
//#include "libopus/opus.h"
|
||||||
|
#include "opusfile/opusfile.h"
|
||||||
|
|
||||||
|
class AudioGeneratorOpus : public AudioGenerator
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
AudioGeneratorOpus();
|
||||||
|
virtual ~AudioGeneratorOpus() override;
|
||||||
|
virtual bool begin(AudioFileSource *source, AudioOutput *output) override;
|
||||||
|
virtual bool loop() override;
|
||||||
|
virtual bool stop() override;
|
||||||
|
virtual bool isRunning() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// Opus callbacks, need static functions to bounce into C++ from C
|
||||||
|
static int OPUS_read(void *_stream, unsigned char *_ptr, int _nbytes) {
|
||||||
|
return static_cast<AudioGeneratorOpus*>(_stream)->read_cb(_ptr, _nbytes);
|
||||||
|
}
|
||||||
|
static int OPUS_seek(void *_stream, opus_int64 _offset, int _whence) {
|
||||||
|
return static_cast<AudioGeneratorOpus*>(_stream)->seek_cb(_offset, _whence);
|
||||||
|
}
|
||||||
|
static opus_int64 OPUS_tell(void *_stream) {
|
||||||
|
return static_cast<AudioGeneratorOpus*>(_stream)->tell_cb();
|
||||||
|
}
|
||||||
|
static int OPUS_close(void *_stream) {
|
||||||
|
return static_cast<AudioGeneratorOpus*>(_stream)->close_cb();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actual Opus callbacks
|
||||||
|
int read_cb(unsigned char *_ptr, int _nbytes);
|
||||||
|
int seek_cb(opus_int64 _offset, int _whence);
|
||||||
|
opus_int64 tell_cb();
|
||||||
|
int close_cb();
|
||||||
|
|
||||||
|
private:
|
||||||
|
OpusFileCallbacks cb = {OPUS_read, OPUS_seek, OPUS_tell, OPUS_close};
|
||||||
|
OggOpusFile *of;
|
||||||
|
int prev_li; // To detect changes in streams
|
||||||
|
|
||||||
|
int16_t *buff;
|
||||||
|
uint32_t buffPtr;
|
||||||
|
uint32_t buffLen;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
|
@ -0,0 +1,292 @@
|
||||||
|
/*
|
||||||
|
AudioGeneratorRTTTL
|
||||||
|
Audio output generator that plays RTTTL (Nokia ringtone)
|
||||||
|
|
||||||
|
Based on the Rtttl Arduino library by James BM, https://github.com/spicajames/Rtttl
|
||||||
|
Based on the gist from Daniel Hall https://gist.github.com/smarthall/1618800
|
||||||
|
|
||||||
|
Copyright (C) 2018 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
#include "AudioGeneratorRTTTL.h"
|
||||||
|
|
||||||
|
AudioGeneratorRTTTL::AudioGeneratorRTTTL()
|
||||||
|
{
|
||||||
|
running = false;
|
||||||
|
file = NULL;
|
||||||
|
output = NULL;
|
||||||
|
rate = 22050;
|
||||||
|
buff = nullptr;
|
||||||
|
ptr = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioGeneratorRTTTL::~AudioGeneratorRTTTL()
|
||||||
|
{
|
||||||
|
free(buff);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioGeneratorRTTTL::stop()
|
||||||
|
{
|
||||||
|
if (!running) return true;
|
||||||
|
running = false;
|
||||||
|
output->stop();
|
||||||
|
return file->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioGeneratorRTTTL::isRunning()
|
||||||
|
{
|
||||||
|
return running;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioGeneratorRTTTL::loop()
|
||||||
|
{
|
||||||
|
if (!running) goto done; // Nothing to do here!
|
||||||
|
|
||||||
|
// Load the next note, if we've hit the end of the last one
|
||||||
|
if (samplesSent == ttlSamples) {
|
||||||
|
if (!GetNextNote()) {
|
||||||
|
running = false;
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
samplesSent = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try and send out the remainder of the existing note, one per loop()
|
||||||
|
if (ttlSamplesPerWaveFP10 == 0) { // Mute
|
||||||
|
int16_t mute[2] = {0, 0};
|
||||||
|
while ((samplesSent < ttlSamples) && output->ConsumeSample(mute)) {
|
||||||
|
samplesSent++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
while (samplesSent < ttlSamples) {
|
||||||
|
int samplesSentFP10 = samplesSent << 10;
|
||||||
|
int rem = samplesSentFP10 % ttlSamplesPerWaveFP10;
|
||||||
|
int16_t val = (rem > ttlSamplesPerWaveFP10/2) ? 8192:-8192;
|
||||||
|
int16_t s[2] = { val, val };
|
||||||
|
if (!output->ConsumeSample(s)) goto done;
|
||||||
|
samplesSent++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
done:
|
||||||
|
file->loop();
|
||||||
|
output->loop();
|
||||||
|
|
||||||
|
return running;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioGeneratorRTTTL::SkipWhitespace()
|
||||||
|
{
|
||||||
|
while ((ptr < len) && (buff[ptr] == ' ')) ptr++;
|
||||||
|
return ptr < len;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioGeneratorRTTTL::ReadInt(int *dest)
|
||||||
|
{
|
||||||
|
if (ptr >= len) return false;
|
||||||
|
|
||||||
|
SkipWhitespace();
|
||||||
|
if (ptr >= len) return false;
|
||||||
|
if ((buff[ptr] <'0') || (buff[ptr] > '9')) return false;
|
||||||
|
|
||||||
|
int t = 0;
|
||||||
|
while ((buff[ptr] >= '0') && (buff[ptr] <='9')) {
|
||||||
|
t = (t * 10) + (buff[ptr] - '0');
|
||||||
|
ptr++;
|
||||||
|
}
|
||||||
|
*dest = t;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool AudioGeneratorRTTTL::ParseHeader()
|
||||||
|
{
|
||||||
|
// Skip the title
|
||||||
|
while ((ptr < len) && (buff[ptr] != ':')) ptr++;
|
||||||
|
if (ptr >= len) return false;
|
||||||
|
if (buff[ptr++] != ':') return false;
|
||||||
|
if (!SkipWhitespace()) return false;
|
||||||
|
if ((buff[ptr] != 'd') && (buff[ptr] != 'D')) return false;
|
||||||
|
ptr++;
|
||||||
|
if (!SkipWhitespace()) return false;
|
||||||
|
if (buff[ptr++] != '=') return false;
|
||||||
|
if (!ReadInt(&defaultDuration)) return false;
|
||||||
|
if (!SkipWhitespace()) return false;
|
||||||
|
if (buff[ptr++] != ',') return false;
|
||||||
|
|
||||||
|
if (!SkipWhitespace()) return false;
|
||||||
|
if ((buff[ptr] != 'o') && (buff[ptr] != 'O')) return false;
|
||||||
|
ptr++;
|
||||||
|
if (!SkipWhitespace()) return false;
|
||||||
|
if (buff[ptr++] != '=') return false;
|
||||||
|
if (!ReadInt(&defaultOctave)) return false;
|
||||||
|
if (!SkipWhitespace()) return false;
|
||||||
|
if (buff[ptr++] != ',') return false;
|
||||||
|
|
||||||
|
int bpm;
|
||||||
|
if (!SkipWhitespace()) return false;
|
||||||
|
if ((buff[ptr] != 'b') && (buff[ptr] != 'B')) return false;
|
||||||
|
ptr++;
|
||||||
|
if (!SkipWhitespace()) return false;
|
||||||
|
if (buff[ptr++] != '=') return false;
|
||||||
|
if (!ReadInt(&bpm)) return false;
|
||||||
|
if (!SkipWhitespace()) return false;
|
||||||
|
if (buff[ptr++] != ':') return false;
|
||||||
|
|
||||||
|
wholeNoteMS = (60 * 1000 * 4) / bpm;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#define NOTE_C4 262
|
||||||
|
#define NOTE_CS4 277
|
||||||
|
#define NOTE_D4 294
|
||||||
|
#define NOTE_DS4 311
|
||||||
|
#define NOTE_E4 330
|
||||||
|
#define NOTE_F4 349
|
||||||
|
#define NOTE_FS4 370
|
||||||
|
#define NOTE_G4 392
|
||||||
|
#define NOTE_GS4 415
|
||||||
|
#define NOTE_A4 440
|
||||||
|
#define NOTE_AS4 466
|
||||||
|
#define NOTE_B4 494
|
||||||
|
#define NOTE_C5 523
|
||||||
|
#define NOTE_CS5 554
|
||||||
|
#define NOTE_D5 587
|
||||||
|
#define NOTE_DS5 622
|
||||||
|
#define NOTE_E5 659
|
||||||
|
#define NOTE_F5 698
|
||||||
|
#define NOTE_FS5 740
|
||||||
|
#define NOTE_G5 784
|
||||||
|
#define NOTE_GS5 831
|
||||||
|
#define NOTE_A5 880
|
||||||
|
#define NOTE_AS5 932
|
||||||
|
#define NOTE_B5 988
|
||||||
|
#define NOTE_C6 1047
|
||||||
|
#define NOTE_CS6 1109
|
||||||
|
#define NOTE_D6 1175
|
||||||
|
#define NOTE_DS6 1245
|
||||||
|
#define NOTE_E6 1319
|
||||||
|
#define NOTE_F6 1397
|
||||||
|
#define NOTE_FS6 1480
|
||||||
|
#define NOTE_G6 1568
|
||||||
|
#define NOTE_GS6 1661
|
||||||
|
#define NOTE_A6 1760
|
||||||
|
#define NOTE_AS6 1865
|
||||||
|
#define NOTE_B6 1976
|
||||||
|
#define NOTE_C7 2093
|
||||||
|
#define NOTE_CS7 2217
|
||||||
|
#define NOTE_D7 2349
|
||||||
|
#define NOTE_DS7 2489
|
||||||
|
#define NOTE_E7 2637
|
||||||
|
#define NOTE_F7 2794
|
||||||
|
#define NOTE_FS7 2960
|
||||||
|
#define NOTE_G7 3136
|
||||||
|
#define NOTE_GS7 3322
|
||||||
|
#define NOTE_A7 3520
|
||||||
|
#define NOTE_AS7 3729
|
||||||
|
#define NOTE_B7 3951
|
||||||
|
static int notes[49] = { 0,
|
||||||
|
NOTE_C4, NOTE_CS4, NOTE_D4, NOTE_DS4, NOTE_E4, NOTE_F4, NOTE_FS4, NOTE_G4, NOTE_GS4, NOTE_A4, NOTE_AS4, NOTE_B4,
|
||||||
|
NOTE_C5, NOTE_CS5, NOTE_D5, NOTE_DS5, NOTE_E5, NOTE_F5, NOTE_FS5, NOTE_G5, NOTE_GS5, NOTE_A5, NOTE_AS5, NOTE_B5,
|
||||||
|
NOTE_C6, NOTE_CS6, NOTE_D6, NOTE_DS6, NOTE_E6, NOTE_F6, NOTE_FS6, NOTE_G6, NOTE_GS6, NOTE_A6, NOTE_AS6, NOTE_B6,
|
||||||
|
NOTE_C7, NOTE_CS7, NOTE_D7, NOTE_DS7, NOTE_E7, NOTE_F7, NOTE_FS7, NOTE_G7, NOTE_GS7, NOTE_A7, NOTE_AS7, NOTE_B7 };
|
||||||
|
|
||||||
|
bool AudioGeneratorRTTTL::GetNextNote()
|
||||||
|
{
|
||||||
|
int dur, note, scale;
|
||||||
|
if (ptr >= len) return false;
|
||||||
|
|
||||||
|
if (!ReadInt(&dur)) {
|
||||||
|
dur = defaultDuration;
|
||||||
|
}
|
||||||
|
dur = wholeNoteMS / dur;
|
||||||
|
|
||||||
|
if (ptr >= len) return false;
|
||||||
|
note = 0;
|
||||||
|
switch (buff[ptr++]) {
|
||||||
|
case 'c': case 'C': note = 1; break;
|
||||||
|
case 'd': case 'D': note = 3; break;
|
||||||
|
case 'e': case 'E': note = 5; break;
|
||||||
|
case 'f': case 'F': note = 6; break;
|
||||||
|
case 'g': case 'G': note = 8; break;
|
||||||
|
case 'a': case 'A': note = 10; break;
|
||||||
|
case 'b': case 'B': note = 12; break;
|
||||||
|
case 'p': case 'P': note = 0; break;
|
||||||
|
default: return false;
|
||||||
|
}
|
||||||
|
if ((ptr < len) && (buff[ptr] == '#')) {
|
||||||
|
ptr++;
|
||||||
|
note++;
|
||||||
|
}
|
||||||
|
if ((ptr < len) && (buff[ptr] == '.')) {
|
||||||
|
ptr++;
|
||||||
|
dur += dur / 2;
|
||||||
|
}
|
||||||
|
if (!ReadInt(&scale)) {
|
||||||
|
scale = defaultOctave;
|
||||||
|
}
|
||||||
|
// Eat any trailing whitespace and comma
|
||||||
|
SkipWhitespace();
|
||||||
|
if ((ptr < len) && (buff[ptr]==',')) {
|
||||||
|
ptr++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scale < 4) scale = 4;
|
||||||
|
if (scale > 7) scale = 7;
|
||||||
|
if (note) {
|
||||||
|
int freq = notes[(scale - 4) * 12 + note];
|
||||||
|
// Convert from frequency in Hz to high and low samples in fixed point
|
||||||
|
ttlSamplesPerWaveFP10 = (rate << 10) / freq;
|
||||||
|
} else {
|
||||||
|
ttlSamplesPerWaveFP10 = 0;
|
||||||
|
}
|
||||||
|
ttlSamples = (rate * dur ) / 1000;
|
||||||
|
|
||||||
|
//audioLogger->printf("%d %d %d %d %d\n", dur, note, scale, ttlSamplesPerWaveFP10, ttlSamples );
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioGeneratorRTTTL::begin(AudioFileSource *source, AudioOutput *output)
|
||||||
|
{
|
||||||
|
if (!source) return false;
|
||||||
|
file = source;
|
||||||
|
if (!output) return false;
|
||||||
|
this->output = output;
|
||||||
|
if (!file->isOpen()) return false; // Error
|
||||||
|
|
||||||
|
len = file->getSize();
|
||||||
|
buff = (char *)malloc(len);
|
||||||
|
if (!buff) return false;
|
||||||
|
if (file->read(buff, len) != (uint32_t)len) return false;
|
||||||
|
|
||||||
|
ptr = 0;
|
||||||
|
samplesSent = 0;
|
||||||
|
ttlSamples = 0;
|
||||||
|
|
||||||
|
if (!ParseHeader()) return false;
|
||||||
|
|
||||||
|
if (!output->SetRate( rate )) return false;
|
||||||
|
if (!output->SetBitsPerSample( 16 )) return false;
|
||||||
|
if (!output->SetChannels( 2 )) return false;
|
||||||
|
if (!output->begin()) return false;
|
||||||
|
|
||||||
|
running = true;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
/*
|
||||||
|
AudioGeneratorRTTTL
|
||||||
|
Audio output generator that plays RTTTL (Nokia ringtones)
|
||||||
|
|
||||||
|
Based on the Rtttl Arduino library by James BM, https://github.com/spicajames/Rtttl
|
||||||
|
Based on the gist from Daniel Hall https://gist.github.com/smarthall/1618800
|
||||||
|
|
||||||
|
Copyright (C) 2018 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _AUDIOGENERATORRTTTL_H
|
||||||
|
#define _AUDIOGENERATORRTTTL_H
|
||||||
|
|
||||||
|
#include "AudioGenerator.h"
|
||||||
|
|
||||||
|
class AudioGeneratorRTTTL : public AudioGenerator
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
AudioGeneratorRTTTL();
|
||||||
|
virtual ~AudioGeneratorRTTTL() override;
|
||||||
|
virtual bool begin(AudioFileSource *source, AudioOutput *output) override;
|
||||||
|
virtual bool loop() override;
|
||||||
|
virtual bool stop() override;
|
||||||
|
virtual bool isRunning() override;
|
||||||
|
void SetRate(uint16_t hz) { rate = hz; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool SkipWhitespace();
|
||||||
|
bool ReadInt(int *dest);
|
||||||
|
bool ParseHeader();
|
||||||
|
bool GetNextNote();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
uint16_t rate;
|
||||||
|
|
||||||
|
// We copy the entire tiny song to a buffer for easier access
|
||||||
|
char *buff;
|
||||||
|
int len;
|
||||||
|
int ptr;
|
||||||
|
|
||||||
|
// Song-global settings
|
||||||
|
int defaultDuration;
|
||||||
|
int defaultOctave;
|
||||||
|
int wholeNoteMS;
|
||||||
|
|
||||||
|
// The note we're currently playing
|
||||||
|
int ttlSamplesPerWaveFP10;
|
||||||
|
int ttlSamples;
|
||||||
|
int samplesSent;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
|
@ -0,0 +1,302 @@
|
||||||
|
/*
|
||||||
|
AudioGeneratorTalkie
|
||||||
|
Audio output generator that speaks using the LPC code in old TI speech chips
|
||||||
|
Output is locked at 8khz as that's that the hardcoded LPC coefficients are built around
|
||||||
|
|
||||||
|
Based on the Talkie Arduino library by Peter Knight, https://github.com/going-digital/Talkie
|
||||||
|
|
||||||
|
Copyright (C) 2020 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
#include "AudioGeneratorTalkie.h"
|
||||||
|
|
||||||
|
AudioGeneratorTalkie::AudioGeneratorTalkie()
|
||||||
|
{
|
||||||
|
running = false;
|
||||||
|
lastFrame = false;
|
||||||
|
file = nullptr;
|
||||||
|
output = nullptr;
|
||||||
|
buff = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioGeneratorTalkie::~AudioGeneratorTalkie()
|
||||||
|
{
|
||||||
|
free(buff);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioGeneratorTalkie::say(const uint8_t *data, size_t len, bool async) {
|
||||||
|
// Finish saying anything in the pipe
|
||||||
|
while (running) {
|
||||||
|
loop();
|
||||||
|
delay(0);
|
||||||
|
}
|
||||||
|
buff = (uint8_t*)realloc(buff, len);
|
||||||
|
if (!buff) return false;
|
||||||
|
memcpy_P(buff, data, len);
|
||||||
|
|
||||||
|
// Reset the interpreter to the start of the stream
|
||||||
|
ptrAddr = buff;
|
||||||
|
ptrBit = 0;
|
||||||
|
frameLeft = 0;
|
||||||
|
lastFrame = false;
|
||||||
|
|
||||||
|
running = true;
|
||||||
|
|
||||||
|
if (!async) {
|
||||||
|
// Finish saying anything in the pipe
|
||||||
|
while (running) {
|
||||||
|
loop();
|
||||||
|
delay(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioGeneratorTalkie::begin(AudioFileSource *source, AudioOutput *output)
|
||||||
|
{
|
||||||
|
if (!output) return false;
|
||||||
|
this->output = output;
|
||||||
|
if (source) {
|
||||||
|
file = source;
|
||||||
|
if (!file->isOpen()) return false; // Error
|
||||||
|
auto len = file->getSize();
|
||||||
|
uint8_t *temp = (uint8_t *)malloc(len);
|
||||||
|
if (!temp) return false;
|
||||||
|
if (file->read(temp, len) != (uint32_t)len) return false;
|
||||||
|
say(temp, len);
|
||||||
|
free(temp);
|
||||||
|
} else {
|
||||||
|
// Reset the interpreter to the start of the stream
|
||||||
|
ptrAddr = buff;
|
||||||
|
ptrBit = 0;
|
||||||
|
frameLeft = 0;
|
||||||
|
running = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!output->SetRate( 8000 )) return false;
|
||||||
|
if (!output->SetBitsPerSample( 16 )) return false;
|
||||||
|
if (!output->SetChannels( 2 )) return false;
|
||||||
|
if (!output->begin()) return false;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool AudioGeneratorTalkie::stop()
|
||||||
|
{
|
||||||
|
if (!running) return true;
|
||||||
|
running = false;
|
||||||
|
output->stop();
|
||||||
|
return file ? file->close() : true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioGeneratorTalkie::isRunning()
|
||||||
|
{
|
||||||
|
return running;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioGeneratorTalkie::loop()
|
||||||
|
{
|
||||||
|
if (!running) goto done; // Nothing to do here!
|
||||||
|
|
||||||
|
if (!frameLeft) {
|
||||||
|
if (lastFrame) {
|
||||||
|
running = false;
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
lastFrame = genOneFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frameLeft) {
|
||||||
|
for ( ; frameLeft; frameLeft--) {
|
||||||
|
auto res = genOneSample();
|
||||||
|
int16_t r[2] = {res, res};
|
||||||
|
if (!output->ConsumeSample(r)) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
done:
|
||||||
|
if (file) file->loop();
|
||||||
|
output->loop();
|
||||||
|
|
||||||
|
return running;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The ROMs used with the TI speech were serial, not byte wide.
|
||||||
|
// Here's a handy routine to flip ROM data which is usually reversed.
|
||||||
|
uint8_t AudioGeneratorTalkie::rev(uint8_t a)
|
||||||
|
{
|
||||||
|
// 76543210
|
||||||
|
a = (a>>4) | (a<<4); // Swap in groups of 4
|
||||||
|
// 32107654
|
||||||
|
a = ((a & 0xcc)>>2) | ((a & 0x33)<<2); // Swap in groups of 2
|
||||||
|
// 10325476
|
||||||
|
a = ((a & 0xaa)>>1) | ((a & 0x55)<<1); // Swap bit pairs
|
||||||
|
// 01234567
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
uint8_t AudioGeneratorTalkie::getBits(uint8_t bits) {
|
||||||
|
uint8_t value;
|
||||||
|
uint16_t data;
|
||||||
|
data = rev(ptrAddr[0])<<8;
|
||||||
|
if (ptrBit+bits > 8) {
|
||||||
|
data |= rev(ptrAddr[1]);
|
||||||
|
}
|
||||||
|
data <<= ptrBit;
|
||||||
|
value = data >> (16-bits);
|
||||||
|
ptrBit += bits;
|
||||||
|
if (ptrBit >= 8) {
|
||||||
|
ptrBit -= 8;
|
||||||
|
ptrAddr++;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#pragma GCC diagnostic push
|
||||||
|
#pragma GCC diagnostic ignored "-Wnarrowing"
|
||||||
|
// Constant LPC coefficient tables
|
||||||
|
static const uint8_t tmsEnergy[0x10] = {0x00,0x02,0x03,0x04,0x05,0x07,0x0a,0x0f,0x14,0x20,0x29,0x39,0x51,0x72,0xa1,0xff};
|
||||||
|
static const uint8_t tmsPeriod[0x40] = {0x00,0x10,0x11,0x12,0x13,0x14,0x15,0x16,0x17,0x18,0x19,0x1A,0x1B,0x1C,0x1D,0x1E,0x1F,0x20,0x21,0x22,0x23,0x24,0x25,0x26,0x27,0x28,0x29,0x2A,0x2B,0x2D,0x2F,0x31,0x33,0x35,0x36,0x39,0x3B,0x3D,0x3F,0x42,0x45,0x47,0x49,0x4D,0x4F,0x51,0x55,0x57,0x5C,0x5F,0x63,0x66,0x6A,0x6E,0x73,0x77,0x7B,0x80,0x85,0x8A,0x8F,0x95,0x9A,0xA0};
|
||||||
|
static const int16_t tmsK1[0x20] = {0x82C0,0x8380,0x83C0,0x8440,0x84C0,0x8540,0x8600,0x8780,0x8880,0x8980,0x8AC0,0x8C00,0x8D40,0x8F00,0x90C0,0x92C0,0x9900,0xA140,0xAB80,0xB840,0xC740,0xD8C0,0xEBC0,0x0000,0x1440,0x2740,0x38C0,0x47C0,0x5480,0x5EC0,0x6700,0x6D40};
|
||||||
|
static const int16_t tmsK2[0x20] = {0xAE00,0xB480,0xBB80,0xC340,0xCB80,0xD440,0xDDC0,0xE780,0xF180,0xFBC0,0x0600,0x1040,0x1A40,0x2400,0x2D40,0x3600,0x3E40,0x45C0,0x4CC0,0x5300,0x5880,0x5DC0,0x6240,0x6640,0x69C0,0x6CC0,0x6F80,0x71C0,0x73C0,0x7580,0x7700,0x7E80};
|
||||||
|
static const int8_t tmsK3[0x10] = {0x92,0x9F,0xAD,0xBA,0xC8,0xD5,0xE3,0xF0,0xFE,0x0B,0x19,0x26,0x34,0x41,0x4F,0x5C};
|
||||||
|
static const int8_t tmsK4[0x10] = {0xAE,0xBC,0xCA,0xD8,0xE6,0xF4,0x01,0x0F,0x1D,0x2B,0x39,0x47,0x55,0x63,0x71,0x7E};
|
||||||
|
static const int8_t tmsK5[0x10] = {0xAE,0xBA,0xC5,0xD1,0xDD,0xE8,0xF4,0xFF,0x0B,0x17,0x22,0x2E,0x39,0x45,0x51,0x5C};
|
||||||
|
static const int8_t tmsK6[0x10] = {0xC0,0xCB,0xD6,0xE1,0xEC,0xF7,0x03,0x0E,0x19,0x24,0x2F,0x3A,0x45,0x50,0x5B,0x66};
|
||||||
|
static const int8_t tmsK7[0x10] = {0xB3,0xBF,0xCB,0xD7,0xE3,0xEF,0xFB,0x07,0x13,0x1F,0x2B,0x37,0x43,0x4F,0x5A,0x66};
|
||||||
|
static const int8_t tmsK8[0x08] = {0xC0,0xD8,0xF0,0x07,0x1F,0x37,0x4F,0x66};
|
||||||
|
static const int8_t tmsK9[0x08] = {0xC0,0xD4,0xE8,0xFC,0x10,0x25,0x39,0x4D};
|
||||||
|
static const int8_t tmsK10[0x08] = {0xCD,0xDF,0xF1,0x04,0x16,0x20,0x3B,0x4D};
|
||||||
|
|
||||||
|
// The chirp we active the filter using
|
||||||
|
static const int8_t chirp[] = {0x00,0x2a,0xd4,0x32,0xb2,0x12,0x25,0x14,0x02,0xe1,0xc5,0x02,0x5f,0x5a,0x05,0x0f,0x26,0xfc,0xa5,0xa5,0xd6,0xdd,0xdc,0xfc,0x25,0x2b,0x22,0x21,0x0f,0xff,0xf8,0xee,0xed,0xef,0xf7,0xf6,0xfa,0x00,0x03,0x02,0x01};
|
||||||
|
#pragma GCC diagnostic pop
|
||||||
|
|
||||||
|
|
||||||
|
bool AudioGeneratorTalkie::genOneFrame() {
|
||||||
|
uint8_t energy;
|
||||||
|
uint8_t repeat;
|
||||||
|
|
||||||
|
// Read speech data, processing the variable size frames.
|
||||||
|
|
||||||
|
energy = getBits(4);
|
||||||
|
if (energy == 0) {
|
||||||
|
// Energy = 0: rest frame
|
||||||
|
synthEnergy = 0;
|
||||||
|
} else if (energy == 0xf) {
|
||||||
|
// Energy = 15: stop frame. Silence the synthesiser.
|
||||||
|
synthEnergy = 0;
|
||||||
|
synthK1 = 0;
|
||||||
|
synthK2 = 0;
|
||||||
|
synthK3 = 0;
|
||||||
|
synthK4 = 0;
|
||||||
|
synthK5 = 0;
|
||||||
|
synthK6 = 0;
|
||||||
|
synthK7 = 0;
|
||||||
|
synthK8 = 0;
|
||||||
|
synthK9 = 0;
|
||||||
|
synthK10 = 0;
|
||||||
|
} else {
|
||||||
|
synthEnergy = tmsEnergy[energy];
|
||||||
|
repeat = getBits(1);
|
||||||
|
synthPeriod = tmsPeriod[getBits(6)];
|
||||||
|
// A repeat frame uses the last coefficients
|
||||||
|
if (!repeat) {
|
||||||
|
// All frames use the first 4 coefficients
|
||||||
|
synthK1 = tmsK1[getBits(5)];
|
||||||
|
synthK2 = tmsK2[getBits(5)];
|
||||||
|
synthK3 = tmsK3[getBits(4)];
|
||||||
|
synthK4 = tmsK4[getBits(4)];
|
||||||
|
if (synthPeriod) {
|
||||||
|
// Voiced frames use 6 extra coefficients.
|
||||||
|
synthK5 = tmsK5[getBits(4)];
|
||||||
|
synthK6 = tmsK6[getBits(4)];
|
||||||
|
synthK7 = tmsK7[getBits(4)];
|
||||||
|
synthK8 = tmsK8[getBits(3)];
|
||||||
|
synthK9 = tmsK9[getBits(3)];
|
||||||
|
synthK10 = tmsK10[getBits(3)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
frameLeft = 8000 / 40;
|
||||||
|
|
||||||
|
return (energy == 0xf); // Last frame will return true
|
||||||
|
}
|
||||||
|
|
||||||
|
int16_t AudioGeneratorTalkie::genOneSample()
|
||||||
|
{
|
||||||
|
static uint8_t periodCounter;
|
||||||
|
static int16_t x0,x1,x2,x3,x4,x5,x6,x7,x8,x9;
|
||||||
|
int16_t u0,u1,u2,u3,u4,u5,u6,u7,u8,u9,u10;
|
||||||
|
|
||||||
|
if (synthPeriod) {
|
||||||
|
// Voiced source
|
||||||
|
if (periodCounter < synthPeriod) {
|
||||||
|
periodCounter++;
|
||||||
|
} else {
|
||||||
|
periodCounter = 0;
|
||||||
|
}
|
||||||
|
if (periodCounter < sizeof(chirp)) {
|
||||||
|
u10 = ((chirp[periodCounter]) * (uint32_t) synthEnergy) >> 8;
|
||||||
|
} else {
|
||||||
|
u10 = 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Unvoiced source
|
||||||
|
static uint16_t synthRand = 1;
|
||||||
|
synthRand = (synthRand >> 1) ^ ((synthRand & 1) ? 0xB800 : 0);
|
||||||
|
u10 = (synthRand & 1) ? synthEnergy : -synthEnergy;
|
||||||
|
}
|
||||||
|
// Lattice filter forward path
|
||||||
|
u9 = u10 - (((int16_t)synthK10*x9) >> 7);
|
||||||
|
u8 = u9 - (((int16_t)synthK9*x8) >> 7);
|
||||||
|
u7 = u8 - (((int16_t)synthK8*x7) >> 7);
|
||||||
|
u6 = u7 - (((int16_t)synthK7*x6) >> 7);
|
||||||
|
u5 = u6 - (((int16_t)synthK6*x5) >> 7);
|
||||||
|
u4 = u5 - (((int16_t)synthK5*x4) >> 7);
|
||||||
|
u3 = u4 - (((int16_t)synthK4*x3) >> 7);
|
||||||
|
u2 = u3 - (((int16_t)synthK3*x2) >> 7);
|
||||||
|
u1 = u2 - (((int32_t)synthK2*x1) >> 15);
|
||||||
|
u0 = u1 - (((int32_t)synthK1*x0) >> 15);
|
||||||
|
|
||||||
|
// Output clamp
|
||||||
|
if (u0 > 511) u0 = 511;
|
||||||
|
if (u0 < -512) u0 = -512;
|
||||||
|
|
||||||
|
// Lattice filter reverse path
|
||||||
|
x9 = x8 + (((int16_t)synthK9*u8) >> 7);
|
||||||
|
x8 = x7 + (((int16_t)synthK8*u7) >> 7);
|
||||||
|
x7 = x6 + (((int16_t)synthK7*u6) >> 7);
|
||||||
|
x6 = x5 + (((int16_t)synthK6*u5) >> 7);
|
||||||
|
x5 = x4 + (((int16_t)synthK5*u4) >> 7);
|
||||||
|
x4 = x3 + (((int16_t)synthK4*u3) >> 7);
|
||||||
|
x3 = x2 + (((int16_t)synthK3*u2) >> 7);
|
||||||
|
x2 = x1 + (((int32_t)synthK2*u1) >> 15);
|
||||||
|
x1 = x0 + (((int32_t)synthK1*u0) >> 15);
|
||||||
|
x0 = u0;
|
||||||
|
|
||||||
|
uint16_t v = u0; // 10 bits
|
||||||
|
v <<= 6; // Now full 16
|
||||||
|
return v;
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
/*
|
||||||
|
AudioGeneratorTalkie
|
||||||
|
Audio output generator that speaks using the LPC code in old TI speech chips
|
||||||
|
Output is locked at 8khz as that's that the hardcoded LPC coefficients are built around
|
||||||
|
|
||||||
|
Based on the Talkie Arduino library by Peter Knight, https://github.com/going-digital/Talkie
|
||||||
|
|
||||||
|
Copyright (C) 2020 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _AUDIOGENERATORTALKIE_H
|
||||||
|
#define _AUDIOGENERATORTALKIE_H
|
||||||
|
|
||||||
|
#include "AudioGenerator.h"
|
||||||
|
|
||||||
|
class AudioGeneratorTalkie : public AudioGenerator
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
AudioGeneratorTalkie();
|
||||||
|
virtual ~AudioGeneratorTalkie() override;
|
||||||
|
virtual bool begin(AudioFileSource *source, AudioOutput *output) override;
|
||||||
|
virtual bool loop() override;
|
||||||
|
virtual bool stop() override;
|
||||||
|
virtual bool isRunning() override;
|
||||||
|
bool say(const uint8_t *data, size_t len, bool async = false);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// The data stream we're playing
|
||||||
|
uint8_t *buff;
|
||||||
|
|
||||||
|
// Codeword stream handlers
|
||||||
|
uint8_t *ptrAddr;
|
||||||
|
uint8_t ptrBit;
|
||||||
|
|
||||||
|
bool lastFrame;
|
||||||
|
bool genOneFrame(); // Fill up one frame's worth of data, returns if this is the last frame
|
||||||
|
int16_t genOneSample(); // Generate one sample of a frame
|
||||||
|
|
||||||
|
// Utilities
|
||||||
|
uint8_t rev(uint8_t a);
|
||||||
|
uint8_t getBits(uint8_t bits);
|
||||||
|
|
||||||
|
// Synthesizer state
|
||||||
|
uint8_t synthPeriod;
|
||||||
|
uint16_t synthEnergy;
|
||||||
|
int16_t synthK1, synthK2;
|
||||||
|
int8_t synthK3, synthK4, synthK5, synthK6, synthK7, synthK8, synthK9, synthK10;
|
||||||
|
|
||||||
|
int frameLeft;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
|
@ -0,0 +1,316 @@
|
||||||
|
/*
|
||||||
|
AudioGeneratorWAV
|
||||||
|
Audio output generator that reads 8 and 16-bit WAV files
|
||||||
|
|
||||||
|
Copyright (C) 2017 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
#include "AudioGeneratorWAV.h"
|
||||||
|
|
||||||
|
AudioGeneratorWAV::AudioGeneratorWAV()
|
||||||
|
{
|
||||||
|
running = false;
|
||||||
|
file = NULL;
|
||||||
|
output = NULL;
|
||||||
|
buffSize = 128;
|
||||||
|
buff = NULL;
|
||||||
|
buffPtr = 0;
|
||||||
|
buffLen = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioGeneratorWAV::~AudioGeneratorWAV()
|
||||||
|
{
|
||||||
|
free(buff);
|
||||||
|
buff = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioGeneratorWAV::stop()
|
||||||
|
{
|
||||||
|
if (!running) return true;
|
||||||
|
running = false;
|
||||||
|
free(buff);
|
||||||
|
buff = NULL;
|
||||||
|
output->stop();
|
||||||
|
return file->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioGeneratorWAV::isRunning()
|
||||||
|
{
|
||||||
|
return running;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Handle buffered reading, reload each time we run out of data
|
||||||
|
bool AudioGeneratorWAV::GetBufferedData(int bytes, void *dest)
|
||||||
|
{
|
||||||
|
if (!running) return false; // Nothing to do here!
|
||||||
|
uint8_t *p = reinterpret_cast<uint8_t*>(dest);
|
||||||
|
while (bytes--) {
|
||||||
|
// Potentially load next batch of data...
|
||||||
|
if (buffPtr >= buffLen) {
|
||||||
|
buffPtr = 0;
|
||||||
|
uint32_t toRead = availBytes > buffSize ? buffSize : availBytes;
|
||||||
|
buffLen = file->read( buff, toRead );
|
||||||
|
availBytes -= buffLen;
|
||||||
|
}
|
||||||
|
if (buffPtr >= buffLen)
|
||||||
|
return false; // No data left!
|
||||||
|
*(p++) = buff[buffPtr++];
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioGeneratorWAV::loop()
|
||||||
|
{
|
||||||
|
if (!running) goto done; // Nothing to do here!
|
||||||
|
|
||||||
|
// First, try and push in the stored sample. If we can't, then punt and try later
|
||||||
|
if (!output->ConsumeSample(lastSample)) goto done; // Can't send, but no error detected
|
||||||
|
|
||||||
|
// Try and stuff the buffer one sample at a time
|
||||||
|
do
|
||||||
|
{
|
||||||
|
if (bitsPerSample == 8) {
|
||||||
|
uint8_t l, r;
|
||||||
|
if (!GetBufferedData(1, &l)) stop();
|
||||||
|
if (channels == 2) {
|
||||||
|
if (!GetBufferedData(1, &r)) stop();
|
||||||
|
} else {
|
||||||
|
r = 0;
|
||||||
|
}
|
||||||
|
lastSample[AudioOutput::LEFTCHANNEL] = l;
|
||||||
|
lastSample[AudioOutput::RIGHTCHANNEL] = r;
|
||||||
|
} else if (bitsPerSample == 16) {
|
||||||
|
if (!GetBufferedData(2, &lastSample[AudioOutput::LEFTCHANNEL])) stop();
|
||||||
|
if (channels == 2) {
|
||||||
|
if (!GetBufferedData(2, &lastSample[AudioOutput::RIGHTCHANNEL])) stop();
|
||||||
|
} else {
|
||||||
|
lastSample[AudioOutput::RIGHTCHANNEL] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (running && output->ConsumeSample(lastSample));
|
||||||
|
|
||||||
|
done:
|
||||||
|
file->loop();
|
||||||
|
output->loop();
|
||||||
|
|
||||||
|
return running;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool AudioGeneratorWAV::ReadWAVInfo()
|
||||||
|
{
|
||||||
|
uint32_t u32;
|
||||||
|
uint16_t u16;
|
||||||
|
int toSkip;
|
||||||
|
|
||||||
|
// WAV specification document:
|
||||||
|
// https://www.aelius.com/njh/wavemetatools/doc/riffmci.pdf
|
||||||
|
|
||||||
|
// Header == "RIFF"
|
||||||
|
if (!ReadU32(&u32)) {
|
||||||
|
Serial.printf_P(PSTR("AudioGeneratorWAV::ReadWAVInfo: failed to read WAV data\n"));
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if (u32 != 0x46464952) {
|
||||||
|
Serial.printf_P(PSTR("AudioGeneratorWAV::ReadWAVInfo: cannot read WAV, invalid RIFF header, got: %08X \n"), (uint32_t) u32);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip ChunkSize
|
||||||
|
if (!ReadU32(&u32)) {
|
||||||
|
Serial.printf_P(PSTR("AudioGeneratorWAV::ReadWAVInfo: failed to read WAV data\n"));
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Format == "WAVE"
|
||||||
|
if (!ReadU32(&u32)) {
|
||||||
|
Serial.printf_P(PSTR("AudioGeneratorWAV::ReadWAVInfo: failed to read WAV data\n"));
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if (u32 != 0x45564157) {
|
||||||
|
Serial.printf_P(PSTR("AudioGeneratorWAV::ReadWAVInfo: cannot read WAV, invalid WAVE header, got: %08X \n"), (uint32_t) u32);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// there might be JUNK or PAD - ignore it by continuing reading until we get to "fmt "
|
||||||
|
while (1) {
|
||||||
|
if (!ReadU32(&u32)) {
|
||||||
|
Serial.printf_P(PSTR("AudioGeneratorWAV::ReadWAVInfo: failed to read WAV data\n"));
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if (u32 == 0x20746d66) break; // 'fmt '
|
||||||
|
};
|
||||||
|
|
||||||
|
// subchunk size
|
||||||
|
if (!ReadU32(&u32)) {
|
||||||
|
Serial.printf_P(PSTR("AudioGeneratorWAV::ReadWAVInfo: failed to read WAV data\n"));
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if (u32 == 16) { toSkip = 0; }
|
||||||
|
else if (u32 == 18) { toSkip = 18 - 16; }
|
||||||
|
else if (u32 == 40) { toSkip = 40 - 16; }
|
||||||
|
else {
|
||||||
|
Serial.printf_P(PSTR("AudioGeneratorWAV::ReadWAVInfo: cannot read WAV, appears not to be standard PCM \n"));
|
||||||
|
return false;
|
||||||
|
} // we only do standard PCM
|
||||||
|
|
||||||
|
// AudioFormat
|
||||||
|
if (!ReadU16(&u16)) {
|
||||||
|
Serial.printf_P(PSTR("AudioGeneratorWAV::ReadWAVInfo: failed to read WAV data\n"));
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if (u16 != 1) {
|
||||||
|
Serial.printf_P(PSTR("AudioGeneratorWAV::ReadWAVInfo: cannot read WAV, AudioFormat appears not to be standard PCM \n"));
|
||||||
|
return false;
|
||||||
|
} // we only do standard PCM
|
||||||
|
|
||||||
|
// NumChannels
|
||||||
|
if (!ReadU16(&channels)) {
|
||||||
|
Serial.printf_P(PSTR("AudioGeneratorWAV::ReadWAVInfo: failed to read WAV data\n"));
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if ((channels<1) || (channels>2)) {
|
||||||
|
Serial.printf_P(PSTR("AudioGeneratorWAV::ReadWAVInfo: cannot read WAV, only mono and stereo are supported \n"));
|
||||||
|
return false;
|
||||||
|
} // Mono or stereo support only
|
||||||
|
|
||||||
|
// SampleRate
|
||||||
|
if (!ReadU32(&sampleRate)) {
|
||||||
|
Serial.printf_P(PSTR("AudioGeneratorWAV::ReadWAVInfo: failed to read WAV data\n"));
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if (sampleRate < 1) {
|
||||||
|
Serial.printf_P(PSTR("AudioGeneratorWAV::ReadWAVInfo: cannot read WAV, unknown sample rate \n"));
|
||||||
|
return false;
|
||||||
|
} // Weird rate, punt. Will need to check w/DAC to see if supported
|
||||||
|
|
||||||
|
// Ignore byterate and blockalign
|
||||||
|
if (!ReadU32(&u32)) {
|
||||||
|
Serial.printf_P(PSTR("AudioGeneratorWAV::ReadWAVInfo: failed to read WAV data\n"));
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if (!ReadU16(&u16)) {
|
||||||
|
Serial.printf_P(PSTR("AudioGeneratorWAV::ReadWAVInfo: failed to read WAV data\n"));
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Bits per sample
|
||||||
|
if (!ReadU16(&bitsPerSample)) {
|
||||||
|
Serial.printf_P(PSTR("AudioGeneratorWAV::ReadWAVInfo: failed to read WAV data\n"));
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if ((bitsPerSample!=8) && (bitsPerSample != 16)) {
|
||||||
|
Serial.printf_P(PSTR("AudioGeneratorWAV::ReadWAVInfo: cannot read WAV, only 8 or 16 bits is supported \n"));
|
||||||
|
return false;
|
||||||
|
} // Only 8 or 16 bits
|
||||||
|
|
||||||
|
// Skip any extra header
|
||||||
|
while (toSkip) {
|
||||||
|
uint8_t ign;
|
||||||
|
if (!ReadU8(&ign)) {
|
||||||
|
Serial.printf_P(PSTR("AudioGeneratorWAV::ReadWAVInfo: failed to read WAV data\n"));
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
toSkip--;
|
||||||
|
}
|
||||||
|
|
||||||
|
// look for data subchunk
|
||||||
|
do {
|
||||||
|
// id == "data"
|
||||||
|
if (!ReadU32(&u32)) {
|
||||||
|
Serial.printf_P(PSTR("AudioGeneratorWAV::ReadWAVInfo: failed to read WAV data\n"));
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if (u32 == 0x61746164) break; // "data"
|
||||||
|
// Skip size, read until end of chunk
|
||||||
|
if (!ReadU32(&u32)) {
|
||||||
|
Serial.printf_P(PSTR("AudioGeneratorWAV::ReadWAVInfo: failed to read WAV data\n"));
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if(!file->seek(u32, SEEK_CUR)) {
|
||||||
|
Serial.printf_P(PSTR("AudioGeneratorWAV::ReadWAVInfo: failed to read WAV data, seek failed\n"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} while (1);
|
||||||
|
if (!file->isOpen()) {
|
||||||
|
Serial.printf_P(PSTR("AudioGeneratorWAV::ReadWAVInfo: cannot read WAV, file is not open\n"));
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Skip size, read until end of file...
|
||||||
|
if (!ReadU32(&u32)) {
|
||||||
|
Serial.printf_P(PSTR("AudioGeneratorWAV::ReadWAVInfo: failed to read WAV data\n"));
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
availBytes = u32;
|
||||||
|
|
||||||
|
// Now set up the buffer or fail
|
||||||
|
buff = reinterpret_cast<uint8_t *>(malloc(buffSize));
|
||||||
|
if (!buff) {
|
||||||
|
Serial.printf_P(PSTR("AudioGeneratorWAV::ReadWAVInfo: cannot read WAV, failed to set up buffer \n"));
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
buffPtr = 0;
|
||||||
|
buffLen = 0;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioGeneratorWAV::begin(AudioFileSource *source, AudioOutput *output)
|
||||||
|
{
|
||||||
|
if (!source) {
|
||||||
|
Serial.printf_P(PSTR("AudioGeneratorWAV::begin: failed: invalid source\n"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
file = source;
|
||||||
|
if (!output) {
|
||||||
|
Serial.printf_P(PSTR("AudioGeneratorWAV::begin: invalid output\n"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this->output = output;
|
||||||
|
if (!file->isOpen()) {
|
||||||
|
Serial.printf_P(PSTR("AudioGeneratorWAV::begin: file not open\n"));
|
||||||
|
return false;
|
||||||
|
} // Error
|
||||||
|
|
||||||
|
if (!ReadWAVInfo()) {
|
||||||
|
Serial.printf_P(PSTR("AudioGeneratorWAV::begin: failed during ReadWAVInfo\n"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!output->SetRate( sampleRate )) {
|
||||||
|
Serial.printf_P(PSTR("AudioGeneratorWAV::begin: failed to SetRate in output\n"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!output->SetBitsPerSample( bitsPerSample )) {
|
||||||
|
Serial.printf_P(PSTR("AudioGeneratorWAV::begin: failed to SetBitsPerSample in output\n"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!output->SetChannels( channels )) {
|
||||||
|
Serial.printf_P(PSTR("AudioGeneratorWAV::begin: failed to SetChannels in output\n"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!output->begin()) {
|
||||||
|
Serial.printf_P(PSTR("AudioGeneratorWAV::begin: output's begin did not return true\n"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
running = true;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
/*
|
||||||
|
AudioGeneratorWAV
|
||||||
|
Audio output generator that reads 8 and 16-bit WAV files
|
||||||
|
|
||||||
|
Copyright (C) 2017 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _AUDIOGENERATORWAV_H
|
||||||
|
#define _AUDIOGENERATORWAV_H
|
||||||
|
|
||||||
|
#include "AudioGenerator.h"
|
||||||
|
|
||||||
|
class AudioGeneratorWAV : public AudioGenerator
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
AudioGeneratorWAV();
|
||||||
|
virtual ~AudioGeneratorWAV() override;
|
||||||
|
virtual bool begin(AudioFileSource *source, AudioOutput *output) override;
|
||||||
|
virtual bool loop() override;
|
||||||
|
virtual bool stop() override;
|
||||||
|
virtual bool isRunning() override;
|
||||||
|
void SetBufferSize(int sz) { buffSize = sz; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool ReadU32(uint32_t *dest) { return file->read(reinterpret_cast<uint8_t*>(dest), 4); }
|
||||||
|
bool ReadU16(uint16_t *dest) { return file->read(reinterpret_cast<uint8_t*>(dest), 2); }
|
||||||
|
bool ReadU8(uint8_t *dest) { return file->read(reinterpret_cast<uint8_t*>(dest), 1); }
|
||||||
|
bool GetBufferedData(int bytes, void *dest);
|
||||||
|
bool ReadWAVInfo();
|
||||||
|
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// WAV info
|
||||||
|
uint16_t channels;
|
||||||
|
uint32_t sampleRate;
|
||||||
|
uint16_t bitsPerSample;
|
||||||
|
|
||||||
|
uint32_t availBytes;
|
||||||
|
|
||||||
|
// We need to buffer some data in-RAM to avoid doing 1000s of small reads
|
||||||
|
uint32_t buffSize;
|
||||||
|
uint8_t *buff;
|
||||||
|
uint16_t buffPtr;
|
||||||
|
uint16_t buffLen;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
|
||||||
|
#include "AudioLogger.h"
|
||||||
|
|
||||||
|
DevNullOut silencedLogger;
|
||||||
|
Print* audioLogger = &silencedLogger;
|
|
@ -0,0 +1,19 @@
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
#ifndef _AUDIOLOGGER_H
|
||||||
|
#define _AUDIOLOGGER_H
|
||||||
|
|
||||||
|
class DevNullOut: public Print
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual size_t write(uint8_t) { return 1; }
|
||||||
|
};
|
||||||
|
|
||||||
|
extern DevNullOut silencedLogger;
|
||||||
|
|
||||||
|
// Global `audioLogger` is initialized to &silencedLogger
|
||||||
|
// It can be initialized anytime to &Serial or any other Print:: derivative instance.
|
||||||
|
extern Print* audioLogger;
|
||||||
|
|
||||||
|
#endif
|
|
@ -0,0 +1,85 @@
|
||||||
|
/*
|
||||||
|
AudioOutput
|
||||||
|
Base class of an audio output player
|
||||||
|
|
||||||
|
Copyright (C) 2017 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _AUDIOOUTPUT_H
|
||||||
|
#define _AUDIOOUTPUT_H
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include "AudioStatus.h"
|
||||||
|
|
||||||
|
class AudioOutput
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
AudioOutput() { };
|
||||||
|
virtual ~AudioOutput() {};
|
||||||
|
virtual bool SetRate(int hz) { hertz = hz; return true; }
|
||||||
|
virtual bool SetBitsPerSample(int bits) { bps = bits; return true; }
|
||||||
|
virtual bool SetChannels(int chan) { channels = chan; return true; }
|
||||||
|
virtual bool SetGain(float f) { if (f>4.0) f = 4.0; if (f<0.0) f=0.0; gainF2P6 = (uint8_t)(f*(1<<6)); return true; }
|
||||||
|
virtual bool begin() { return false; };
|
||||||
|
typedef enum { LEFTCHANNEL=0, RIGHTCHANNEL=1 } SampleIndex;
|
||||||
|
virtual bool ConsumeSample(int16_t sample[2]) { (void)sample; return false; }
|
||||||
|
virtual uint16_t ConsumeSamples(int16_t *samples, uint16_t count)
|
||||||
|
{
|
||||||
|
for (uint16_t i=0; i<count; i++) {
|
||||||
|
if (!ConsumeSample(samples)) return i;
|
||||||
|
samples += 2;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
virtual bool stop() { return false; }
|
||||||
|
virtual void flush() { return; }
|
||||||
|
virtual bool loop() { return true; }
|
||||||
|
|
||||||
|
public:
|
||||||
|
virtual bool RegisterMetadataCB(AudioStatus::metadataCBFn fn, void *data) { return cb.RegisterMetadataCB(fn, data); }
|
||||||
|
virtual bool RegisterStatusCB(AudioStatus::statusCBFn fn, void *data) { return cb.RegisterStatusCB(fn, data); }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void MakeSampleStereo16(int16_t sample[2]) {
|
||||||
|
// Mono to "stereo" conversion
|
||||||
|
if (channels == 1)
|
||||||
|
sample[RIGHTCHANNEL] = sample[LEFTCHANNEL];
|
||||||
|
if (bps == 8) {
|
||||||
|
// Upsample from unsigned 8 bits to signed 16 bits
|
||||||
|
sample[LEFTCHANNEL] = (((int16_t)(sample[LEFTCHANNEL]&0xff)) - 128) << 8;
|
||||||
|
sample[RIGHTCHANNEL] = (((int16_t)(sample[RIGHTCHANNEL]&0xff)) - 128) << 8;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
inline int16_t Amplify(int16_t s) {
|
||||||
|
int32_t v = (s * gainF2P6)>>6;
|
||||||
|
if (v < -32767) return -32767;
|
||||||
|
else if (v > 32767) return 32767;
|
||||||
|
else return (int16_t)(v&0xffff);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
uint16_t hertz;
|
||||||
|
uint8_t bps;
|
||||||
|
uint8_t channels;
|
||||||
|
uint8_t gainF2P6; // Fixed point 2.6
|
||||||
|
|
||||||
|
protected:
|
||||||
|
AudioStatus cb;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
/*
|
||||||
|
AudioOutputBuffer
|
||||||
|
Adds additional bufferspace to the output chain
|
||||||
|
|
||||||
|
Copyright (C) 2017 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include "AudioOutputBuffer.h"
|
||||||
|
|
||||||
|
AudioOutputBuffer::AudioOutputBuffer(int buffSizeSamples, AudioOutput *dest)
|
||||||
|
{
|
||||||
|
buffSize = buffSizeSamples;
|
||||||
|
leftSample = (int16_t*)malloc(sizeof(int16_t) * buffSize);
|
||||||
|
rightSample = (int16_t*)malloc(sizeof(int16_t) * buffSize);
|
||||||
|
writePtr = 0;
|
||||||
|
readPtr = 0;
|
||||||
|
sink = dest;
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioOutputBuffer::~AudioOutputBuffer()
|
||||||
|
{
|
||||||
|
free(leftSample);
|
||||||
|
free(rightSample);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioOutputBuffer::SetRate(int hz)
|
||||||
|
{
|
||||||
|
return sink->SetRate(hz);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioOutputBuffer::SetBitsPerSample(int bits)
|
||||||
|
{
|
||||||
|
return sink->SetBitsPerSample(bits);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioOutputBuffer::SetChannels(int channels)
|
||||||
|
{
|
||||||
|
return sink->SetChannels(channels);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioOutputBuffer::begin()
|
||||||
|
{
|
||||||
|
filled = false;
|
||||||
|
return sink->begin();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioOutputBuffer::ConsumeSample(int16_t sample[2])
|
||||||
|
{
|
||||||
|
// First, try and fill I2S...
|
||||||
|
if (filled) {
|
||||||
|
while (readPtr != writePtr) {
|
||||||
|
int16_t s[2] = {leftSample[readPtr], rightSample[readPtr]};
|
||||||
|
if (!sink->ConsumeSample(s)) break; // Can't stuff any more in I2S...
|
||||||
|
readPtr = (readPtr + 1) % buffSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now, do we have space for a new sample?
|
||||||
|
int nextWritePtr = (writePtr + 1) % buffSize;
|
||||||
|
if (nextWritePtr == readPtr) {
|
||||||
|
filled = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
leftSample[writePtr] = sample[LEFTCHANNEL];
|
||||||
|
rightSample[writePtr] = sample[RIGHTCHANNEL];
|
||||||
|
writePtr = nextWritePtr;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioOutputBuffer::stop()
|
||||||
|
{
|
||||||
|
return sink->stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
/*
|
||||||
|
AudioOutputBuffer
|
||||||
|
Adds additional bufferspace to the output chain
|
||||||
|
|
||||||
|
Copyright (C) 2017 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _AUDIOOUTPUTBUFFER_H
|
||||||
|
#define _AUDIOOUTPUTBUFFER_H
|
||||||
|
|
||||||
|
#include "AudioOutput.h"
|
||||||
|
|
||||||
|
class AudioOutputBuffer : public AudioOutput
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
AudioOutputBuffer(int bufferSizeSamples, AudioOutput *dest);
|
||||||
|
virtual ~AudioOutputBuffer() override;
|
||||||
|
virtual bool SetRate(int hz) override;
|
||||||
|
virtual bool SetBitsPerSample(int bits) override;
|
||||||
|
virtual bool SetChannels(int channels) override;
|
||||||
|
virtual bool begin() override;
|
||||||
|
virtual bool ConsumeSample(int16_t sample[2]) override;
|
||||||
|
virtual bool stop() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
AudioOutput *sink;
|
||||||
|
int buffSize;
|
||||||
|
int16_t *leftSample;
|
||||||
|
int16_t *rightSample;
|
||||||
|
int writePtr;
|
||||||
|
int readPtr;
|
||||||
|
bool filled;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
|
@ -0,0 +1,112 @@
|
||||||
|
/*
|
||||||
|
AudioOutputFilter
|
||||||
|
Implements a user-defined FIR on a passthrough
|
||||||
|
|
||||||
|
Copyright (C) 2017 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include "AudioOutputFilterDecimate.h"
|
||||||
|
|
||||||
|
AudioOutputFilterDecimate::AudioOutputFilterDecimate(uint8_t taps, const int16_t *tap, int num, int den, AudioOutput *sink)
|
||||||
|
{
|
||||||
|
this->sink = sink;
|
||||||
|
|
||||||
|
// The filter state. Passed in TAPS must be available throughout object lifetime
|
||||||
|
this->taps = taps;
|
||||||
|
this->tap = (int16_t*)malloc(sizeof(int16_t) * taps);
|
||||||
|
memcpy_P(this->tap, tap, sizeof(int16_t) * taps);
|
||||||
|
this->hist[0] = (int16_t*)malloc(sizeof(int16_t) * taps);
|
||||||
|
memset(this->hist[0], 0, sizeof(int16_t) * taps);
|
||||||
|
this->hist[1] = (int16_t*)malloc(sizeof(int16_t) * taps);
|
||||||
|
memset(this->hist[1], 0, sizeof(int16_t) * taps);
|
||||||
|
this->idx = 0;
|
||||||
|
|
||||||
|
// Decimator numerator and denominator with an error signal. Not great, but fast and simple
|
||||||
|
this->num = num;
|
||||||
|
this->den = den;
|
||||||
|
this->err = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioOutputFilterDecimate::~AudioOutputFilterDecimate()
|
||||||
|
{
|
||||||
|
free(hist[1]);
|
||||||
|
free(hist[0]);
|
||||||
|
free(tap);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioOutputFilterDecimate::SetRate(int hz)
|
||||||
|
{
|
||||||
|
// Modify input frequency to account for decimation
|
||||||
|
hz *= den;
|
||||||
|
hz /= num;
|
||||||
|
return sink->SetRate(hz);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioOutputFilterDecimate::SetBitsPerSample(int bits)
|
||||||
|
{
|
||||||
|
return sink->SetBitsPerSample(bits);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioOutputFilterDecimate::SetChannels(int channels)
|
||||||
|
{
|
||||||
|
return sink->SetChannels(channels);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioOutputFilterDecimate::SetGain(float gain)
|
||||||
|
{
|
||||||
|
return sink->SetGain(gain);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioOutputFilterDecimate::begin()
|
||||||
|
{
|
||||||
|
return sink->begin();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioOutputFilterDecimate::ConsumeSample(int16_t sample[2])
|
||||||
|
{
|
||||||
|
// Store the data samples in history always
|
||||||
|
hist[LEFTCHANNEL][idx] = sample[LEFTCHANNEL];
|
||||||
|
hist[RIGHTCHANNEL][idx] = sample[RIGHTCHANNEL];
|
||||||
|
idx++;
|
||||||
|
if (idx == taps) idx = 0;
|
||||||
|
|
||||||
|
// Only output if the error signal says we're ready to decimate. This simplistic way might give some aliasing noise
|
||||||
|
err += num;
|
||||||
|
if (err >= den) {
|
||||||
|
err -= den;
|
||||||
|
// Need to output a sample, so actually calculate the filter at this point in time
|
||||||
|
// Smarter might actually shift the history by the fractional remainder or take two filters and interpolate
|
||||||
|
int32_t accL = 0;
|
||||||
|
int32_t accR = 0;
|
||||||
|
int index = idx;
|
||||||
|
for (size_t i=0; i < taps; i++) {
|
||||||
|
index = index != 0 ? index-1 : taps-1;
|
||||||
|
accL += (int32_t)hist[LEFTCHANNEL][index] * tap[i];
|
||||||
|
accR += (int32_t)hist[RIGHTCHANNEL][index] * tap[i];
|
||||||
|
};
|
||||||
|
int16_t out[2];
|
||||||
|
out[LEFTCHANNEL] = accL >> 16;
|
||||||
|
out[RIGHTCHANNEL] = accR >> 16;
|
||||||
|
return sink->ConsumeSample(out);
|
||||||
|
}
|
||||||
|
return true; // Nothing to do here...
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioOutputFilterDecimate::stop()
|
||||||
|
{
|
||||||
|
return sink->stop();
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
/*
|
||||||
|
AudioOutputFilterDecimate
|
||||||
|
Implements a user-defined FIR on a passthrough w/rational decimation
|
||||||
|
|
||||||
|
Copyright (C) 2017 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _AUDIOOUTPUTFILTERDECIMATE_H
|
||||||
|
#define _AUDIOOUTPUTFILTERDECIMATE_H
|
||||||
|
|
||||||
|
#include "AudioOutput.h"
|
||||||
|
|
||||||
|
class AudioOutputFilterDecimate : public AudioOutput
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
AudioOutputFilterDecimate(uint8_t taps, const int16_t *tap, int num, int den, AudioOutput *sink);
|
||||||
|
virtual ~AudioOutputFilterDecimate() override;
|
||||||
|
virtual bool SetRate(int hz) override;
|
||||||
|
virtual bool SetBitsPerSample(int bits) override;
|
||||||
|
virtual bool SetChannels(int chan) override;
|
||||||
|
virtual bool SetGain(float f) override;
|
||||||
|
virtual bool begin() override;
|
||||||
|
virtual bool ConsumeSample(int16_t sample[2]) override;
|
||||||
|
virtual bool stop() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
AudioOutput *sink;
|
||||||
|
uint8_t taps;
|
||||||
|
int16_t *tap;
|
||||||
|
int16_t *hist[2];
|
||||||
|
int idx;
|
||||||
|
int num;
|
||||||
|
int den;
|
||||||
|
int err;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
|
@ -0,0 +1,223 @@
|
||||||
|
/*
|
||||||
|
AudioOutputI2S
|
||||||
|
Base class for I2S interface port
|
||||||
|
|
||||||
|
Copyright (C) 2017 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#ifdef ESP32
|
||||||
|
#include "driver/i2s.h"
|
||||||
|
#else
|
||||||
|
#include <i2s.h>
|
||||||
|
#endif
|
||||||
|
#include "AudioOutputI2S.h"
|
||||||
|
|
||||||
|
AudioOutputI2S::AudioOutputI2S(int port, int output_mode, int dma_buf_count, int use_apll)
|
||||||
|
{
|
||||||
|
this->portNo = port;
|
||||||
|
this->i2sOn = false;
|
||||||
|
this->dma_buf_count = dma_buf_count;
|
||||||
|
if (output_mode != EXTERNAL_I2S && output_mode != INTERNAL_DAC && output_mode != INTERNAL_PDM) {
|
||||||
|
output_mode = EXTERNAL_I2S;
|
||||||
|
}
|
||||||
|
this->output_mode = output_mode;
|
||||||
|
#ifdef ESP32
|
||||||
|
if (!i2sOn) {
|
||||||
|
if (use_apll == APLL_AUTO) {
|
||||||
|
// don't use audio pll on buggy rev0 chips
|
||||||
|
use_apll = APLL_DISABLE;
|
||||||
|
esp_chip_info_t out_info;
|
||||||
|
esp_chip_info(&out_info);
|
||||||
|
if(out_info.revision > 0) {
|
||||||
|
use_apll = APLL_ENABLE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
i2s_mode_t mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX);
|
||||||
|
if (output_mode == INTERNAL_DAC) {
|
||||||
|
mode = (i2s_mode_t)(mode | I2S_MODE_DAC_BUILT_IN);
|
||||||
|
} else if (output_mode == INTERNAL_PDM) {
|
||||||
|
mode = (i2s_mode_t)(mode | I2S_MODE_PDM);
|
||||||
|
}
|
||||||
|
|
||||||
|
i2s_comm_format_t comm_fmt = (i2s_comm_format_t)(I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB);
|
||||||
|
if (output_mode == INTERNAL_DAC) {
|
||||||
|
comm_fmt = (i2s_comm_format_t)I2S_COMM_FORMAT_I2S_MSB;
|
||||||
|
}
|
||||||
|
|
||||||
|
i2s_config_t i2s_config_dac = {
|
||||||
|
.mode = mode,
|
||||||
|
.sample_rate = 44100,
|
||||||
|
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
|
||||||
|
.channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
|
||||||
|
.communication_format = comm_fmt,
|
||||||
|
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, // lowest interrupt priority
|
||||||
|
.dma_buf_count = dma_buf_count,
|
||||||
|
.dma_buf_len = 64,
|
||||||
|
.use_apll = use_apll // Use audio PLL
|
||||||
|
};
|
||||||
|
audioLogger->printf("+%d %p\n", portNo, &i2s_config_dac);
|
||||||
|
if (i2s_driver_install((i2s_port_t)portNo, &i2s_config_dac, 0, NULL) != ESP_OK) {
|
||||||
|
audioLogger->println("ERROR: Unable to install I2S drives\n");
|
||||||
|
}
|
||||||
|
if (output_mode == INTERNAL_DAC || output_mode == INTERNAL_PDM) {
|
||||||
|
i2s_set_pin((i2s_port_t)portNo, NULL);
|
||||||
|
i2s_set_dac_mode(I2S_DAC_CHANNEL_BOTH_EN);
|
||||||
|
} else {
|
||||||
|
SetPinout(26, 25, 22);
|
||||||
|
}
|
||||||
|
i2s_zero_dma_buffer((i2s_port_t)portNo);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
(void) dma_buf_count;
|
||||||
|
(void) use_apll;
|
||||||
|
if (!i2sOn) {
|
||||||
|
orig_bck = READ_PERI_REG(PERIPHS_IO_MUX_MTDO_U);
|
||||||
|
orig_ws = READ_PERI_REG(PERIPHS_IO_MUX_GPIO2_U);
|
||||||
|
i2s_begin();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
i2sOn = true;
|
||||||
|
mono = false;
|
||||||
|
bps = 16;
|
||||||
|
channels = 2;
|
||||||
|
SetGain(1.0);
|
||||||
|
SetRate(44100); // Default
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioOutputI2S::~AudioOutputI2S()
|
||||||
|
{
|
||||||
|
#ifdef ESP32
|
||||||
|
if (i2sOn) {
|
||||||
|
audioLogger->printf("UNINSTALL I2S\n");
|
||||||
|
i2s_driver_uninstall((i2s_port_t)portNo); //stop & destroy i2s driver
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
if (i2sOn) i2s_end();
|
||||||
|
#endif
|
||||||
|
i2sOn = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioOutputI2S::SetPinout(int bclk, int wclk, int dout)
|
||||||
|
{
|
||||||
|
#ifdef ESP32
|
||||||
|
if (output_mode == INTERNAL_DAC || output_mode == INTERNAL_PDM) return false; // Not allowed
|
||||||
|
|
||||||
|
i2s_pin_config_t pins = {
|
||||||
|
.bck_io_num = bclk,
|
||||||
|
.ws_io_num = wclk,
|
||||||
|
.data_out_num = dout,
|
||||||
|
.data_in_num = I2S_PIN_NO_CHANGE
|
||||||
|
};
|
||||||
|
i2s_set_pin((i2s_port_t)portNo, &pins);
|
||||||
|
return true;
|
||||||
|
#else
|
||||||
|
(void) bclk;
|
||||||
|
(void) wclk;
|
||||||
|
(void) dout;
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioOutputI2S::SetRate(int hz)
|
||||||
|
{
|
||||||
|
// TODO - have a list of allowable rates from constructor, check them
|
||||||
|
this->hertz = hz;
|
||||||
|
#ifdef ESP32
|
||||||
|
i2s_set_sample_rates((i2s_port_t)portNo, AdjustI2SRate(hz));
|
||||||
|
#else
|
||||||
|
i2s_set_rate(AdjustI2SRate(hz));
|
||||||
|
#endif
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioOutputI2S::SetBitsPerSample(int bits)
|
||||||
|
{
|
||||||
|
if ( (bits != 16) && (bits != 8) ) return false;
|
||||||
|
this->bps = bits;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioOutputI2S::SetChannels(int channels)
|
||||||
|
{
|
||||||
|
if ( (channels < 1) || (channels > 2) ) return false;
|
||||||
|
this->channels = channels;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioOutputI2S::SetOutputModeMono(bool mono)
|
||||||
|
{
|
||||||
|
this->mono = mono;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioOutputI2S::begin()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioOutputI2S::ConsumeSample(int16_t sample[2])
|
||||||
|
{
|
||||||
|
int16_t ms[2];
|
||||||
|
|
||||||
|
ms[0] = sample[0];
|
||||||
|
ms[1] = sample[1];
|
||||||
|
MakeSampleStereo16( ms );
|
||||||
|
|
||||||
|
if (this->mono) {
|
||||||
|
// Average the two samples and overwrite
|
||||||
|
int32_t ttl = ms[LEFTCHANNEL] + ms[RIGHTCHANNEL];
|
||||||
|
ms[LEFTCHANNEL] = ms[RIGHTCHANNEL] = (ttl>>1) & 0xffff;
|
||||||
|
}
|
||||||
|
#ifdef ESP32
|
||||||
|
uint32_t s32;
|
||||||
|
if (output_mode == INTERNAL_DAC) {
|
||||||
|
int16_t l = Amplify(ms[LEFTCHANNEL]) + 0x8000;
|
||||||
|
int16_t r = Amplify(ms[RIGHTCHANNEL]) + 0x8000;
|
||||||
|
s32 = (r<<16) | (l&0xffff);
|
||||||
|
} else {
|
||||||
|
s32 = ((Amplify(ms[RIGHTCHANNEL]))<<16) | (Amplify(ms[LEFTCHANNEL]) & 0xffff);
|
||||||
|
}
|
||||||
|
return i2s_write_bytes((i2s_port_t)portNo, (const char*)&s32, sizeof(uint32_t), 0);
|
||||||
|
#else
|
||||||
|
uint32_t s32 = ((Amplify(ms[RIGHTCHANNEL]))<<16) | (Amplify(ms[LEFTCHANNEL]) & 0xffff);
|
||||||
|
return i2s_write_sample_nb(s32); // If we can't store it, return false. OTW true
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioOutputI2S::flush() {
|
||||||
|
#ifdef ESP32
|
||||||
|
// makes sure that all stored DMA samples are consumed / played
|
||||||
|
int buffersize = 64 * this->dma_buf_count;
|
||||||
|
int16_t samples[2] = {0x0,0x0};
|
||||||
|
for (int i=0;i<buffersize; i++) {
|
||||||
|
while (!ConsumeSample(samples)) {
|
||||||
|
delay(10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioOutputI2S::stop()
|
||||||
|
{
|
||||||
|
#ifdef ESP32
|
||||||
|
i2s_zero_dma_buffer((i2s_port_t)portNo);
|
||||||
|
#endif
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
AudioOutputI2S
|
||||||
|
Base class for an I2S output port
|
||||||
|
|
||||||
|
Copyright (C) 2017 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _AUDIOOUTPUTI2S_H
|
||||||
|
#define _AUDIOOUTPUTI2S_H
|
||||||
|
|
||||||
|
#include "AudioOutput.h"
|
||||||
|
|
||||||
|
class AudioOutputI2S : public AudioOutput
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
AudioOutputI2S(int port=0, int output_mode=EXTERNAL_I2S, int dma_buf_count = 8, int use_apll=APLL_DISABLE);
|
||||||
|
virtual ~AudioOutputI2S() override;
|
||||||
|
bool SetPinout(int bclkPin, int wclkPin, int doutPin);
|
||||||
|
virtual bool SetRate(int hz) override;
|
||||||
|
virtual bool SetBitsPerSample(int bits) override;
|
||||||
|
virtual bool SetChannels(int channels) override;
|
||||||
|
virtual bool begin() override;
|
||||||
|
virtual bool ConsumeSample(int16_t sample[2]) override;
|
||||||
|
virtual void flush() override;
|
||||||
|
virtual bool stop() override;
|
||||||
|
|
||||||
|
bool SetOutputModeMono(bool mono); // Force mono output no matter the input
|
||||||
|
|
||||||
|
enum : int { APLL_AUTO = -1, APLL_ENABLE = 1, APLL_DISABLE = 0 };
|
||||||
|
enum : int { EXTERNAL_I2S = 0, INTERNAL_DAC = 1, INTERNAL_PDM = 2 };
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual int AdjustI2SRate(int hz) { return hz; }
|
||||||
|
uint8_t portNo;
|
||||||
|
int output_mode;
|
||||||
|
bool mono;
|
||||||
|
bool i2sOn;
|
||||||
|
int dma_buf_count;
|
||||||
|
// We can restore the old values and free up these pins when in NoDAC mode
|
||||||
|
uint32_t orig_bck;
|
||||||
|
uint32_t orig_ws;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
|
@ -0,0 +1,108 @@
|
||||||
|
/*
|
||||||
|
AudioOutputI2SNoDAC
|
||||||
|
Audio player using SW delta-sigma to generate "analog" on I2S data
|
||||||
|
|
||||||
|
Copyright (C) 2017 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#ifdef ESP32
|
||||||
|
#include "driver/i2s.h"
|
||||||
|
#else
|
||||||
|
#include <i2s.h>
|
||||||
|
#endif
|
||||||
|
#include "AudioOutputI2SNoDAC.h"
|
||||||
|
|
||||||
|
|
||||||
|
AudioOutputI2SNoDAC::AudioOutputI2SNoDAC(int port) : AudioOutputI2S(port, false)
|
||||||
|
{
|
||||||
|
SetOversampling(32);
|
||||||
|
lastSamp = 0;
|
||||||
|
cumErr = 0;
|
||||||
|
#ifndef ESP32
|
||||||
|
WRITE_PERI_REG(PERIPHS_IO_MUX_MTDO_U, orig_bck);
|
||||||
|
WRITE_PERI_REG(PERIPHS_IO_MUX_GPIO2_U, orig_ws);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioOutputI2SNoDAC::~AudioOutputI2SNoDAC()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioOutputI2SNoDAC::SetOversampling(int os) {
|
||||||
|
if (os % 32) return false; // Only Nx32 oversampling supported
|
||||||
|
if (os > 256) return false; // Don't be silly now!
|
||||||
|
if (os < 32) return false; // Nothing under 32 allowed
|
||||||
|
|
||||||
|
oversample = os;
|
||||||
|
return SetRate(hertz);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioOutputI2SNoDAC::DeltaSigma(int16_t sample[2], uint32_t dsBuff[8])
|
||||||
|
{
|
||||||
|
// Not shift 8 because addition takes care of one mult x 2
|
||||||
|
int32_t sum = (((int32_t)sample[0]) + ((int32_t)sample[1])) >> 1;
|
||||||
|
fixed24p8_t newSamp = ( (int32_t)Amplify(sum) ) << 8;
|
||||||
|
|
||||||
|
int oversample32 = oversample / 32;
|
||||||
|
// How much the comparison signal changes each oversample step
|
||||||
|
fixed24p8_t diffPerStep = (newSamp - lastSamp) >> (4 + oversample32);
|
||||||
|
|
||||||
|
// Don't need lastSamp anymore, store this one for next round
|
||||||
|
lastSamp = newSamp;
|
||||||
|
|
||||||
|
for (int j = 0; j < oversample32; j++) {
|
||||||
|
uint32_t bits = 0; // The bits we convert the sample into, MSB to go on the wire first
|
||||||
|
|
||||||
|
for (int i = 32; i > 0; i--) {
|
||||||
|
bits = bits << 1;
|
||||||
|
if (cumErr < 0) {
|
||||||
|
bits |= 1;
|
||||||
|
cumErr += fixedPosValue - newSamp;
|
||||||
|
} else {
|
||||||
|
// Bits[0] = 0 handled already by left shift
|
||||||
|
cumErr -= fixedPosValue + newSamp;
|
||||||
|
}
|
||||||
|
newSamp += diffPerStep; // Move the reference signal towards destination
|
||||||
|
}
|
||||||
|
dsBuff[j] = bits;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioOutputI2SNoDAC::ConsumeSample(int16_t sample[2])
|
||||||
|
{
|
||||||
|
int16_t ms[2];
|
||||||
|
ms[0] = sample[0];
|
||||||
|
ms[1] = sample[1];
|
||||||
|
MakeSampleStereo16( ms );
|
||||||
|
|
||||||
|
// Make delta-sigma filled buffer
|
||||||
|
uint32_t dsBuff[8];
|
||||||
|
DeltaSigma(ms, dsBuff);
|
||||||
|
|
||||||
|
// Either send complete pulse stream or nothing
|
||||||
|
#ifdef ESP32
|
||||||
|
if (!i2s_write_bytes((i2s_port_t)portNo, (const char *)dsBuff, sizeof(uint32_t) * (oversample/32), 0))
|
||||||
|
return false;
|
||||||
|
#else
|
||||||
|
if (!i2s_write_sample_nb(dsBuff[0])) return false; // No room at the inn
|
||||||
|
// At this point we've sent in first of possibly 8 32-bits, need to send
|
||||||
|
// remaining ones even if they block.
|
||||||
|
for (int i = 32; i < oversample; i+=32)
|
||||||
|
i2s_write_sample( dsBuff[i / 32]);
|
||||||
|
#endif
|
||||||
|
return true;
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
/*
|
||||||
|
AudioOutputI2SNoDAC
|
||||||
|
Audio player using SW delta-sigma to generate "analog" on I2S data
|
||||||
|
|
||||||
|
Copyright (C) 2017 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _AUDIOOUTPUTI2SNODAC_H
|
||||||
|
#define _AUDIOOUTPUTI2SNODAC_H
|
||||||
|
|
||||||
|
#include "AudioOutputI2S.h"
|
||||||
|
|
||||||
|
class AudioOutputI2SNoDAC : public AudioOutputI2S
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
AudioOutputI2SNoDAC(int port = 0);
|
||||||
|
virtual ~AudioOutputI2SNoDAC() override;
|
||||||
|
virtual bool ConsumeSample(int16_t sample[2]) override;
|
||||||
|
|
||||||
|
bool SetOversampling(int os);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual int AdjustI2SRate(int hz) override { return hz * oversample/32; }
|
||||||
|
uint8_t oversample;
|
||||||
|
void DeltaSigma(int16_t sample[2], uint32_t dsBuff[4]);
|
||||||
|
typedef int32_t fixed24p8_t;
|
||||||
|
enum {fixedPosValue=0x007fff00}; /* 24.8 of max-signed-int */
|
||||||
|
fixed24p8_t lastSamp; // Last sample value
|
||||||
|
fixed24p8_t cumErr; // Running cumulative error since time began
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
|
@ -0,0 +1,244 @@
|
||||||
|
/*
|
||||||
|
AudioOutputMixer
|
||||||
|
Simple mixer which can combine multiple inputs to a single output stream
|
||||||
|
|
||||||
|
Copyright (C) 2018 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include "AudioOutputMixer.h"
|
||||||
|
|
||||||
|
AudioOutputMixerStub::AudioOutputMixerStub(AudioOutputMixer *sink, int id) : AudioOutput()
|
||||||
|
{
|
||||||
|
this->id = id;
|
||||||
|
this->parent = sink;
|
||||||
|
SetGain(1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioOutputMixerStub::~AudioOutputMixerStub()
|
||||||
|
{
|
||||||
|
parent->RemoveInput(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioOutputMixerStub::SetRate(int hz)
|
||||||
|
{
|
||||||
|
return parent->SetRate(hz, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioOutputMixerStub::SetBitsPerSample(int bits)
|
||||||
|
{
|
||||||
|
return parent->SetBitsPerSample(bits, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioOutputMixerStub::SetChannels(int channels)
|
||||||
|
{
|
||||||
|
return parent->SetChannels(channels, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioOutputMixerStub::begin()
|
||||||
|
{
|
||||||
|
return parent->begin(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioOutputMixerStub::ConsumeSample(int16_t sample[2])
|
||||||
|
{
|
||||||
|
int16_t amp[2];
|
||||||
|
amp[LEFTCHANNEL] = Amplify(sample[LEFTCHANNEL]);
|
||||||
|
amp[RIGHTCHANNEL] = Amplify(sample[RIGHTCHANNEL]);
|
||||||
|
return parent->ConsumeSample(amp, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioOutputMixerStub::stop()
|
||||||
|
{
|
||||||
|
return parent->stop(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
AudioOutputMixer::AudioOutputMixer(int buffSizeSamples, AudioOutput *dest) : AudioOutput()
|
||||||
|
{
|
||||||
|
buffSize = buffSizeSamples;
|
||||||
|
leftAccum = (int32_t*)calloc(sizeof(int32_t), buffSize);
|
||||||
|
rightAccum = (int32_t*)calloc(sizeof(int32_t), buffSize);
|
||||||
|
for (int i=0; i<maxStubs; i++) {
|
||||||
|
stubAllocated[i] = false;
|
||||||
|
stubRunning[i] = false;
|
||||||
|
writePtr[i] = 0;
|
||||||
|
}
|
||||||
|
readPtr = 0;
|
||||||
|
sink = dest;
|
||||||
|
sinkStarted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioOutputMixer::~AudioOutputMixer()
|
||||||
|
{
|
||||||
|
free(leftAccum);
|
||||||
|
free(rightAccum);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Most "standard" interfaces should fail, only MixerStub should be able to talk to us
|
||||||
|
bool AudioOutputMixer::SetRate(int hz)
|
||||||
|
{
|
||||||
|
(void) hz;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioOutputMixer::SetBitsPerSample(int bits)
|
||||||
|
{
|
||||||
|
(void) bits;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioOutputMixer::SetChannels(int channels)
|
||||||
|
{
|
||||||
|
(void) channels;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioOutputMixer::ConsumeSample(int16_t sample[2])
|
||||||
|
{
|
||||||
|
(void) sample;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioOutputMixer::begin()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioOutputMixer::stop()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// TODO - actually ensure all samples are same speed, size, channels, rate
|
||||||
|
bool AudioOutputMixer::SetRate(int hz, int id)
|
||||||
|
{
|
||||||
|
(void) id;
|
||||||
|
return sink->SetRate(hz);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioOutputMixer::SetBitsPerSample(int bits, int id)
|
||||||
|
{
|
||||||
|
(void) id;
|
||||||
|
return sink->SetBitsPerSample(bits);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioOutputMixer::SetChannels(int channels, int id)
|
||||||
|
{
|
||||||
|
(void) id;
|
||||||
|
return sink->SetChannels(channels);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioOutputMixer::begin(int id)
|
||||||
|
{
|
||||||
|
stubRunning[id] = true;
|
||||||
|
|
||||||
|
if (!sinkStarted) {
|
||||||
|
sinkStarted = true;
|
||||||
|
return sink->begin();
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioOutputMixerStub *AudioOutputMixer::NewInput()
|
||||||
|
{
|
||||||
|
for (int i=0; i<maxStubs; i++) {
|
||||||
|
if (!stubAllocated[i]) {
|
||||||
|
stubAllocated[i] = true;
|
||||||
|
stubRunning[i] = false;
|
||||||
|
writePtr[i] = readPtr; // TODO - should it be 1 before readPtr?
|
||||||
|
AudioOutputMixerStub *stub = new AudioOutputMixerStub(this, i);
|
||||||
|
return stub;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioOutputMixer::RemoveInput(int id)
|
||||||
|
{
|
||||||
|
stubAllocated[id] = false;
|
||||||
|
stubRunning[id] = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioOutputMixer::loop()
|
||||||
|
{
|
||||||
|
// First, try and fill I2S...
|
||||||
|
// This is not optimal, but algorithmically should work fine
|
||||||
|
bool avail;
|
||||||
|
do {
|
||||||
|
avail = true;
|
||||||
|
for (int i=0; i<maxStubs && avail; i++) {
|
||||||
|
if (stubRunning[i] && writePtr[i] == readPtr) {
|
||||||
|
avail = false; // The read pointer is touching an active writer, can't advance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (avail) {
|
||||||
|
int16_t s[2];
|
||||||
|
if (leftAccum[readPtr] > 32767) {
|
||||||
|
s[LEFTCHANNEL] = 32767;
|
||||||
|
} else if (leftAccum[readPtr] < -32767) {
|
||||||
|
s[LEFTCHANNEL] = -32767;
|
||||||
|
} else {
|
||||||
|
s[LEFTCHANNEL] = leftAccum[readPtr];
|
||||||
|
}
|
||||||
|
if (rightAccum[readPtr] > 32767) {
|
||||||
|
s[RIGHTCHANNEL] = 32767;
|
||||||
|
} else if (rightAccum[readPtr] < -32767) {
|
||||||
|
s[RIGHTCHANNEL] = -32767;
|
||||||
|
} else {
|
||||||
|
s[RIGHTCHANNEL] = rightAccum[readPtr];
|
||||||
|
}
|
||||||
|
// s[LEFTCHANNEL] = Amplify(s[LEFTCHANNEL]);
|
||||||
|
// s[RIGHTCHANNEL] = Amplify(s[RIGHTCHANNEL]);
|
||||||
|
if (!sink->ConsumeSample(s)) {
|
||||||
|
break; // Can't stuff any more in I2S...
|
||||||
|
}
|
||||||
|
// Clear the accums and advance the pointer to next potential sample
|
||||||
|
leftAccum[readPtr] = 0;
|
||||||
|
rightAccum[readPtr] = 0;
|
||||||
|
readPtr = (readPtr + 1) % buffSize;
|
||||||
|
}
|
||||||
|
} while (avail);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioOutputMixer::ConsumeSample(int16_t sample[2], int id)
|
||||||
|
{
|
||||||
|
loop(); // Send any pre-existing, completed I2S data we can fit
|
||||||
|
|
||||||
|
// Now, do we have space for a new sample?
|
||||||
|
int nextWritePtr = (writePtr[id] + 1) % buffSize;
|
||||||
|
if (nextWritePtr == readPtr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
leftAccum[writePtr[id]] += sample[LEFTCHANNEL];
|
||||||
|
rightAccum[writePtr[id]] += sample[RIGHTCHANNEL];
|
||||||
|
writePtr[id] = nextWritePtr;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioOutputMixer::stop(int id)
|
||||||
|
{
|
||||||
|
stubRunning[id] = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,88 @@
|
||||||
|
/*
|
||||||
|
AudioOutputMixer
|
||||||
|
Simple mixer which can combine multiple inputs to a single output stream
|
||||||
|
|
||||||
|
Copyright (C) 2017 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _AUDIOOUTPUTMIXER_H
|
||||||
|
#define _AUDIOOUTPUTMIXER_H
|
||||||
|
|
||||||
|
#include "AudioOutput.h"
|
||||||
|
|
||||||
|
class AudioOutputMixer;
|
||||||
|
|
||||||
|
|
||||||
|
// The output stub exported by the mixer for use by the generator
|
||||||
|
class AudioOutputMixerStub : public AudioOutput
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
AudioOutputMixerStub(AudioOutputMixer *sink, int id);
|
||||||
|
virtual ~AudioOutputMixerStub() override;
|
||||||
|
virtual bool SetRate(int hz) override;
|
||||||
|
virtual bool SetBitsPerSample(int bits) override;
|
||||||
|
virtual bool SetChannels(int channels) override;
|
||||||
|
virtual bool begin() override;
|
||||||
|
virtual bool ConsumeSample(int16_t sample[2]) override;
|
||||||
|
virtual bool stop() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
AudioOutputMixer *parent;
|
||||||
|
int id;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Single mixer object per output
|
||||||
|
class AudioOutputMixer : public AudioOutput
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
AudioOutputMixer(int samples, AudioOutput *sink);
|
||||||
|
virtual ~AudioOutputMixer() override;
|
||||||
|
virtual bool SetRate(int hz) override;
|
||||||
|
virtual bool SetBitsPerSample(int bits) override;
|
||||||
|
virtual bool SetChannels(int channels) override;
|
||||||
|
virtual bool begin() override;
|
||||||
|
virtual bool ConsumeSample(int16_t sample[2]) override;
|
||||||
|
virtual bool stop() override;
|
||||||
|
virtual bool loop() override; // Send all existing samples we can to I2S
|
||||||
|
|
||||||
|
AudioOutputMixerStub *NewInput(); // Get a new stub to pass to a generator
|
||||||
|
|
||||||
|
// Stub called functions
|
||||||
|
friend class AudioOutputMixerStub;
|
||||||
|
private:
|
||||||
|
void RemoveInput(int id);
|
||||||
|
bool SetRate(int hz, int id);
|
||||||
|
bool SetBitsPerSample(int bits, int id);
|
||||||
|
bool SetChannels(int channels, int id);
|
||||||
|
bool begin(int id);
|
||||||
|
bool ConsumeSample(int16_t sample[2], int id);
|
||||||
|
bool stop(int id);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
enum { maxStubs = 8 };
|
||||||
|
AudioOutput *sink;
|
||||||
|
bool sinkStarted;
|
||||||
|
int16_t buffSize;
|
||||||
|
int32_t *leftAccum;
|
||||||
|
int32_t *rightAccum;
|
||||||
|
bool stubAllocated[maxStubs];
|
||||||
|
bool stubRunning[maxStubs];
|
||||||
|
int16_t writePtr[maxStubs]; // Array of pointers for allocated stubs
|
||||||
|
int16_t readPtr;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
AudioOutput
|
||||||
|
Base class of an audio output player
|
||||||
|
|
||||||
|
Copyright (C) 2017 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _AUDIOOUTPUTNULL_H
|
||||||
|
#define _AUDIOOUTPUTNULL_H
|
||||||
|
|
||||||
|
#include "AudioOutput.h"
|
||||||
|
|
||||||
|
class AudioOutputNull : public AudioOutput
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
AudioOutputNull() {};
|
||||||
|
~AudioOutputNull() {};
|
||||||
|
virtual bool begin() { samples = 0; startms = millis(); return true; }
|
||||||
|
virtual bool ConsumeSample(int16_t sample[2]) { (void)sample; samples++; return true; }
|
||||||
|
virtual bool stop() { endms = millis(); return true; };
|
||||||
|
unsigned long GetMilliseconds() { return endms - startms; }
|
||||||
|
int GetSamples() { return samples; }
|
||||||
|
int GetFrequency() { return hertz; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
unsigned long startms;
|
||||||
|
unsigned long endms;
|
||||||
|
int samples;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
|
@ -0,0 +1,288 @@
|
||||||
|
/*
|
||||||
|
AudioOutputSPDIF
|
||||||
|
|
||||||
|
S/PDIF output via I2S
|
||||||
|
|
||||||
|
Needs transciever from CMOS level to either optical or coaxial interface
|
||||||
|
See: https://www.epanorama.net/documents/audio/spdif.html
|
||||||
|
|
||||||
|
Original idea and sources:
|
||||||
|
Forum thread dicussing implementation
|
||||||
|
https://forum.pjrc.com/threads/28639-S-pdif
|
||||||
|
Teensy Audio Library
|
||||||
|
https://github.com/PaulStoffregen/Audio/blob/master/output_spdif2.cpp
|
||||||
|
|
||||||
|
Adapted for ESP8266Audio
|
||||||
|
|
||||||
|
NOTE: This module operates I2S at 4x sampling rate, as it needs to
|
||||||
|
send out each bit as two output symbols, packed into
|
||||||
|
32-bit words. Even for mono sound, S/PDIF is specified minimum
|
||||||
|
for 2 channels, each as 32-bits sub-frame. This drains I2S
|
||||||
|
buffers 4x more quickly so you may need 4x bigger output
|
||||||
|
buffers than usual, configurable with 'dma_buf_count'
|
||||||
|
constructor parameter.
|
||||||
|
|
||||||
|
Copyright (C) 2020 Ivan Kostoski
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#if defined(ESP32)
|
||||||
|
#include "driver/i2s.h"
|
||||||
|
#include "soc/rtc.h"
|
||||||
|
#elif defined(ESP8266)
|
||||||
|
#include "driver/SinglePinI2SDriver.h"
|
||||||
|
#endif
|
||||||
|
#include "AudioOutputSPDIF.h"
|
||||||
|
|
||||||
|
// BMC (Biphase Mark Coded) values (bit order reversed, i.e. LSB first)
|
||||||
|
static const uint16_t spdif_bmclookup[256] PROGMEM = {
|
||||||
|
0xcccc, 0x4ccc, 0x2ccc, 0xaccc, 0x34cc, 0xb4cc, 0xd4cc, 0x54cc,
|
||||||
|
0x32cc, 0xb2cc, 0xd2cc, 0x52cc, 0xcacc, 0x4acc, 0x2acc, 0xaacc,
|
||||||
|
0x334c, 0xb34c, 0xd34c, 0x534c, 0xcb4c, 0x4b4c, 0x2b4c, 0xab4c,
|
||||||
|
0xcd4c, 0x4d4c, 0x2d4c, 0xad4c, 0x354c, 0xb54c, 0xd54c, 0x554c,
|
||||||
|
0x332c, 0xb32c, 0xd32c, 0x532c, 0xcb2c, 0x4b2c, 0x2b2c, 0xab2c,
|
||||||
|
0xcd2c, 0x4d2c, 0x2d2c, 0xad2c, 0x352c, 0xb52c, 0xd52c, 0x552c,
|
||||||
|
0xccac, 0x4cac, 0x2cac, 0xacac, 0x34ac, 0xb4ac, 0xd4ac, 0x54ac,
|
||||||
|
0x32ac, 0xb2ac, 0xd2ac, 0x52ac, 0xcaac, 0x4aac, 0x2aac, 0xaaac,
|
||||||
|
0x3334, 0xb334, 0xd334, 0x5334, 0xcb34, 0x4b34, 0x2b34, 0xab34,
|
||||||
|
0xcd34, 0x4d34, 0x2d34, 0xad34, 0x3534, 0xb534, 0xd534, 0x5534,
|
||||||
|
0xccb4, 0x4cb4, 0x2cb4, 0xacb4, 0x34b4, 0xb4b4, 0xd4b4, 0x54b4,
|
||||||
|
0x32b4, 0xb2b4, 0xd2b4, 0x52b4, 0xcab4, 0x4ab4, 0x2ab4, 0xaab4,
|
||||||
|
0xccd4, 0x4cd4, 0x2cd4, 0xacd4, 0x34d4, 0xb4d4, 0xd4d4, 0x54d4,
|
||||||
|
0x32d4, 0xb2d4, 0xd2d4, 0x52d4, 0xcad4, 0x4ad4, 0x2ad4, 0xaad4,
|
||||||
|
0x3354, 0xb354, 0xd354, 0x5354, 0xcb54, 0x4b54, 0x2b54, 0xab54,
|
||||||
|
0xcd54, 0x4d54, 0x2d54, 0xad54, 0x3554, 0xb554, 0xd554, 0x5554,
|
||||||
|
0x3332, 0xb332, 0xd332, 0x5332, 0xcb32, 0x4b32, 0x2b32, 0xab32,
|
||||||
|
0xcd32, 0x4d32, 0x2d32, 0xad32, 0x3532, 0xb532, 0xd532, 0x5532,
|
||||||
|
0xccb2, 0x4cb2, 0x2cb2, 0xacb2, 0x34b2, 0xb4b2, 0xd4b2, 0x54b2,
|
||||||
|
0x32b2, 0xb2b2, 0xd2b2, 0x52b2, 0xcab2, 0x4ab2, 0x2ab2, 0xaab2,
|
||||||
|
0xccd2, 0x4cd2, 0x2cd2, 0xacd2, 0x34d2, 0xb4d2, 0xd4d2, 0x54d2,
|
||||||
|
0x32d2, 0xb2d2, 0xd2d2, 0x52d2, 0xcad2, 0x4ad2, 0x2ad2, 0xaad2,
|
||||||
|
0x3352, 0xb352, 0xd352, 0x5352, 0xcb52, 0x4b52, 0x2b52, 0xab52,
|
||||||
|
0xcd52, 0x4d52, 0x2d52, 0xad52, 0x3552, 0xb552, 0xd552, 0x5552,
|
||||||
|
0xccca, 0x4cca, 0x2cca, 0xacca, 0x34ca, 0xb4ca, 0xd4ca, 0x54ca,
|
||||||
|
0x32ca, 0xb2ca, 0xd2ca, 0x52ca, 0xcaca, 0x4aca, 0x2aca, 0xaaca,
|
||||||
|
0x334a, 0xb34a, 0xd34a, 0x534a, 0xcb4a, 0x4b4a, 0x2b4a, 0xab4a,
|
||||||
|
0xcd4a, 0x4d4a, 0x2d4a, 0xad4a, 0x354a, 0xb54a, 0xd54a, 0x554a,
|
||||||
|
0x332a, 0xb32a, 0xd32a, 0x532a, 0xcb2a, 0x4b2a, 0x2b2a, 0xab2a,
|
||||||
|
0xcd2a, 0x4d2a, 0x2d2a, 0xad2a, 0x352a, 0xb52a, 0xd52a, 0x552a,
|
||||||
|
0xccaa, 0x4caa, 0x2caa, 0xacaa, 0x34aa, 0xb4aa, 0xd4aa, 0x54aa,
|
||||||
|
0x32aa, 0xb2aa, 0xd2aa, 0x52aa, 0xcaaa, 0x4aaa, 0x2aaa, 0xaaaa
|
||||||
|
};
|
||||||
|
|
||||||
|
AudioOutputSPDIF::AudioOutputSPDIF(int dout_pin, int port, int dma_buf_count)
|
||||||
|
{
|
||||||
|
this->portNo = port;
|
||||||
|
#if defined(ESP32)
|
||||||
|
// Configure ESP32 I2S to roughly compatible to ESP8266 peripheral
|
||||||
|
i2s_config_t i2s_config_spdif = {
|
||||||
|
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX),
|
||||||
|
.sample_rate = 88200, // 2 x sampling_rate
|
||||||
|
.bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT, // 32bit words
|
||||||
|
.channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT, // Right than left
|
||||||
|
.communication_format = (i2s_comm_format_t)(I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB),
|
||||||
|
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, // lowest interrupt priority
|
||||||
|
.dma_buf_count = dma_buf_count,
|
||||||
|
.dma_buf_len = DMA_BUF_SIZE_DEFAULT, // bigger buffers, reduces interrupts
|
||||||
|
.use_apll = true // Audio PLL is needed for low clock jitter
|
||||||
|
};
|
||||||
|
if (i2s_driver_install((i2s_port_t)portNo, &i2s_config_spdif, 0, NULL) != ESP_OK) {
|
||||||
|
audioLogger->println(F("ERROR: Unable to install I2S drivers"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
i2s_zero_dma_buffer((i2s_port_t)portNo);
|
||||||
|
SetPinout(I2S_PIN_NO_CHANGE, I2S_PIN_NO_CHANGE, dout_pin);
|
||||||
|
rate_multiplier = 2; // 2x32bit words
|
||||||
|
#elif defined(ESP8266)
|
||||||
|
(void) dout_pin;
|
||||||
|
if (!I2SDriver.begin(dma_buf_count, DMA_BUF_SIZE_DEFAULT)) {
|
||||||
|
audioLogger->println(F("ERROR: Unable to start I2S driver"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rate_multiplier = 4; // 4x16 bit words
|
||||||
|
#endif
|
||||||
|
i2sOn = true;
|
||||||
|
mono = false;
|
||||||
|
bps = 16;
|
||||||
|
channels = 2;
|
||||||
|
frame_num = 0;
|
||||||
|
SetGain(1.0);
|
||||||
|
hertz = 0;
|
||||||
|
SetRate(44100);
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioOutputSPDIF::~AudioOutputSPDIF()
|
||||||
|
{
|
||||||
|
#if defined(ESP32)
|
||||||
|
if (i2sOn) {
|
||||||
|
i2s_stop((i2s_port_t)this->portNo);
|
||||||
|
audioLogger->printf("UNINSTALL I2S\n");
|
||||||
|
i2s_driver_uninstall((i2s_port_t)this->portNo); //stop & destroy i2s driver
|
||||||
|
}
|
||||||
|
#elif defined(ESP8266)
|
||||||
|
if (i2sOn) I2SDriver.stop();
|
||||||
|
#endif
|
||||||
|
i2sOn = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioOutputSPDIF::SetPinout(int bclk, int wclk, int dout)
|
||||||
|
{
|
||||||
|
#if defined(ESP32)
|
||||||
|
i2s_pin_config_t pins = {
|
||||||
|
.bck_io_num = bclk,
|
||||||
|
.ws_io_num = wclk,
|
||||||
|
.data_out_num = dout,
|
||||||
|
.data_in_num = I2S_PIN_NO_CHANGE
|
||||||
|
};
|
||||||
|
if (i2s_set_pin((i2s_port_t)portNo, &pins) != ESP_OK) {
|
||||||
|
audioLogger->println("ERROR setting up S/PDIF I2S pins\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
#else
|
||||||
|
(void) bclk;
|
||||||
|
(void) wclk;
|
||||||
|
(void) dout;
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioOutputSPDIF::SetRate(int hz)
|
||||||
|
{
|
||||||
|
if (!i2sOn) return false;
|
||||||
|
if (hz < 32000) return false;
|
||||||
|
if (hz == this->hertz) return true;
|
||||||
|
this->hertz = hz;
|
||||||
|
int adjustedHz = AdjustI2SRate(hz);
|
||||||
|
#if defined(ESP32)
|
||||||
|
if (i2s_set_sample_rates((i2s_port_t)portNo, adjustedHz) == ESP_OK) {
|
||||||
|
if (adjustedHz == 88200) {
|
||||||
|
// Manually fix the APLL rate for 44100.
|
||||||
|
// See: https://github.com/espressif/esp-idf/issues/2634
|
||||||
|
// sdm0 = 28, sdm1 = 8, sdm2 = 5, odir = 0 -> 88199.977
|
||||||
|
rtc_clk_apll_enable(1, 28, 8, 5, 0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
audioLogger->println("ERROR changing S/PDIF sample rate");
|
||||||
|
}
|
||||||
|
#elif defined(ESP8266)
|
||||||
|
I2SDriver.setRate(adjustedHz);
|
||||||
|
audioLogger->printf_P(PSTR("S/PDIF rate set: %.3f\n"), I2SDriver.getActualRate()/4);
|
||||||
|
#endif
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioOutputSPDIF::SetBitsPerSample(int bits)
|
||||||
|
{
|
||||||
|
if ( (bits != 16) && (bits != 8) ) return false;
|
||||||
|
this->bps = bits;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioOutputSPDIF::SetChannels(int channels)
|
||||||
|
{
|
||||||
|
if ( (channels < 1) || (channels > 2) ) return false;
|
||||||
|
this->channels = channels;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioOutputSPDIF::SetOutputModeMono(bool mono)
|
||||||
|
{
|
||||||
|
this->mono = mono;
|
||||||
|
// Just use the left channel for mono
|
||||||
|
if (mono) SetChannels(1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioOutputSPDIF::begin()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioOutputSPDIF::ConsumeSample(int16_t sample[2])
|
||||||
|
{
|
||||||
|
if (!i2sOn) return true; // Sink the data
|
||||||
|
int16_t ms[2];
|
||||||
|
uint16_t hi, lo, aux;
|
||||||
|
uint32_t buf[4];
|
||||||
|
|
||||||
|
ms[0] = sample[0];
|
||||||
|
ms[1] = sample[1];
|
||||||
|
MakeSampleStereo16(ms);
|
||||||
|
|
||||||
|
// S/PDIF encoding:
|
||||||
|
// http://www.hardwarebook.info/S/PDIF
|
||||||
|
// Original sources: Teensy Audio Library
|
||||||
|
// https://github.com/PaulStoffregen/Audio/blob/master/output_spdif2.cpp
|
||||||
|
//
|
||||||
|
// Order of bits, before BMC encoding, from the definition of SPDIF format
|
||||||
|
// PPPP AAAA SSSS SSSS SSSS SSSS SSSS VUCP
|
||||||
|
// are sent rearanged as
|
||||||
|
// VUCP PPPP AAAA 0000 SSSS SSSS SSSS SSSS
|
||||||
|
// This requires a bit less shifting as 16 sample bits align and can be
|
||||||
|
// BMC encoded with two table lookups (and at the same time flipped to LSB first).
|
||||||
|
// There is no separate word-clock, so hopefully the receiver won't notice.
|
||||||
|
|
||||||
|
uint16_t sample_left = Amplify(ms[LEFTCHANNEL]);
|
||||||
|
// BMC encode and flip left channel bits
|
||||||
|
hi = pgm_read_word(&spdif_bmclookup[(uint8_t)(sample_left >> 8)]);
|
||||||
|
lo = pgm_read_word(&spdif_bmclookup[(uint8_t)sample_left]);
|
||||||
|
// Low word is inverted depending on first bit of high word
|
||||||
|
lo ^= (~((int16_t)hi) >> 16);
|
||||||
|
buf[0] = ((uint32_t)lo << 16) | hi;
|
||||||
|
// Fixed 4 bits auxillary-audio-databits, the first used as parity
|
||||||
|
// Depending on first bit of low word, invert the bits
|
||||||
|
aux = 0xb333 ^ (((uint32_t)((int16_t)lo)) >> 17);
|
||||||
|
// Send 'B' preamble only for the first frame of data-block
|
||||||
|
if (frame_num == 0) {
|
||||||
|
buf[1] = VUCP_PREAMBLE_B | aux;
|
||||||
|
} else {
|
||||||
|
buf[1] = VUCP_PREAMBLE_M | aux;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t sample_right = Amplify(ms[RIGHTCHANNEL]);
|
||||||
|
// BMC encode right channel, similar as above
|
||||||
|
hi = pgm_read_word(&spdif_bmclookup[(uint8_t)(sample_right >> 8)]);
|
||||||
|
lo = pgm_read_word(&spdif_bmclookup[(uint8_t)sample_right]);
|
||||||
|
lo ^= (~((int16_t)hi) >> 16);
|
||||||
|
buf[2] = ((uint32_t)lo << 16) | hi;
|
||||||
|
aux = 0xb333 ^ (((uint32_t)((int16_t)lo)) >> 17);
|
||||||
|
buf[3] = VUCP_PREAMBLE_W | aux;
|
||||||
|
|
||||||
|
#if defined(ESP32)
|
||||||
|
// Assume DMA buffers are multiples of 16 bytes. Either we write all bytes or none.
|
||||||
|
uint32_t bytes_written;
|
||||||
|
esp_err_t ret = i2s_write((i2s_port_t)portNo, (const char*)&buf, 8 * channels, &bytes_written, 0);
|
||||||
|
// If we didn't write all bytes, return false early and do not increment frame_num
|
||||||
|
if ((ret != ESP_OK) || (bytes_written != (8 * channels))) return false;
|
||||||
|
#elif defined(ESP8266)
|
||||||
|
if (!I2SDriver.writeInterleaved(buf)) return false;
|
||||||
|
#endif
|
||||||
|
// Increment and rotate frame number
|
||||||
|
if (++frame_num > 191) frame_num = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioOutputSPDIF::stop()
|
||||||
|
{
|
||||||
|
#if defined(ESP32)
|
||||||
|
i2s_zero_dma_buffer((i2s_port_t)portNo);
|
||||||
|
#elif defined(ESP8266)
|
||||||
|
I2SDriver.stop();
|
||||||
|
#endif
|
||||||
|
frame_num = 0;
|
||||||
|
return true;
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
/*
|
||||||
|
AudioOutputSPDIF
|
||||||
|
|
||||||
|
S/PDIF output via I2S
|
||||||
|
|
||||||
|
Needs transciever from CMOS level to either optical or coaxial interface
|
||||||
|
See: https://www.epanorama.net/documents/audio/spdif.html
|
||||||
|
|
||||||
|
Original idea and sources:
|
||||||
|
Forum thread dicussing implementation
|
||||||
|
https://forum.pjrc.com/threads/28639-S-pdif
|
||||||
|
Teensy Audio Library
|
||||||
|
https://github.com/PaulStoffregen/Audio/blob/master/output_spdif2.cpp
|
||||||
|
|
||||||
|
Adapted for ESP8266Audio
|
||||||
|
|
||||||
|
Copyright (C) 2020 Ivan Kostoski
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _AUDIOOUTPUTSPDIF_H
|
||||||
|
#define _AUDIOOUTPUTSPDIF_H
|
||||||
|
|
||||||
|
#include "AudioOutput.h"
|
||||||
|
|
||||||
|
#if defined(ESP32)
|
||||||
|
#define SPDIF_OUT_PIN_DEFAULT 27
|
||||||
|
#define DMA_BUF_COUNT_DEFAULT 8
|
||||||
|
#define DMA_BUF_SIZE_DEFAULT 256
|
||||||
|
#elif defined(ESP8266)
|
||||||
|
#define SPDIF_OUT_PIN_DEFAULT 3
|
||||||
|
#define DMA_BUF_COUNT_DEFAULT 32
|
||||||
|
#define DMA_BUF_SIZE_DEFAULT 64
|
||||||
|
#endif
|
||||||
|
|
||||||
|
class AudioOutputSPDIF : public AudioOutput
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
AudioOutputSPDIF(int dout_pin=SPDIF_OUT_PIN_DEFAULT, int port=0, int dma_buf_count = DMA_BUF_COUNT_DEFAULT);
|
||||||
|
virtual ~AudioOutputSPDIF() override;
|
||||||
|
bool SetPinout(int bclkPin, int wclkPin, int doutPin);
|
||||||
|
virtual bool SetRate(int hz) override;
|
||||||
|
virtual bool SetBitsPerSample(int bits) override;
|
||||||
|
virtual bool SetChannels(int channels) override;
|
||||||
|
virtual bool begin() override;
|
||||||
|
virtual bool ConsumeSample(int16_t sample[2]) override;
|
||||||
|
virtual bool stop() override;
|
||||||
|
|
||||||
|
bool SetOutputModeMono(bool mono); // Force mono output no matter the input
|
||||||
|
|
||||||
|
const uint32_t VUCP_PREAMBLE_B = 0xCCE80000; // 11001100 11101000
|
||||||
|
const uint32_t VUCP_PREAMBLE_M = 0xCCE20000; // 11001100 11100010
|
||||||
|
const uint32_t VUCP_PREAMBLE_W = 0xCCE40000; // 11001100 11100100
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual inline int AdjustI2SRate(int hz) { return rate_multiplier * hz; }
|
||||||
|
uint8_t portNo;
|
||||||
|
bool mono;
|
||||||
|
bool i2sOn;
|
||||||
|
uint8_t frame_num;
|
||||||
|
uint8_t rate_multiplier;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // _AUDIOOUTPUTSPDIF_H
|
|
@ -0,0 +1,113 @@
|
||||||
|
/*
|
||||||
|
AudioOutputSPIFFSWAV
|
||||||
|
Writes a WAV file to the SPIFFS filesystem
|
||||||
|
|
||||||
|
Copyright (C) 2017 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <FS.h>
|
||||||
|
#ifdef ESP32
|
||||||
|
#include "SPIFFS.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "AudioOutputSPIFFSWAV.h"
|
||||||
|
|
||||||
|
static const uint8_t wavHeaderTemplate[] PROGMEM = { // Hardcoded simple WAV header with 0xffffffff lengths all around
|
||||||
|
0x52, 0x49, 0x46, 0x46, 0xff, 0xff, 0xff, 0xff, 0x57, 0x41, 0x56, 0x45,
|
||||||
|
0x66, 0x6d, 0x74, 0x20, 0x10, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x22, 0x56, 0x00, 0x00, 0x88, 0x58, 0x01, 0x00, 0x04, 0x00, 0x10, 0x00,
|
||||||
|
0x64, 0x61, 0x74, 0x61, 0xff, 0xff, 0xff, 0xff };
|
||||||
|
|
||||||
|
void AudioOutputSPIFFSWAV::SetFilename(const char *name)
|
||||||
|
{
|
||||||
|
if (filename) free(filename);
|
||||||
|
filename = strdup(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioOutputSPIFFSWAV::begin()
|
||||||
|
{
|
||||||
|
uint8_t wavHeader[sizeof(wavHeaderTemplate)];
|
||||||
|
memset(wavHeader, 0, sizeof(wavHeader));
|
||||||
|
|
||||||
|
if (f) return false; // Already open!
|
||||||
|
SPIFFS.remove(filename);
|
||||||
|
f = SPIFFS.open(filename, "w+");
|
||||||
|
if (!f) return false;
|
||||||
|
|
||||||
|
// We'll fix the header up when we close the file
|
||||||
|
f.write(wavHeader, sizeof(wavHeader));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioOutputSPIFFSWAV::ConsumeSample(int16_t sample[2])
|
||||||
|
{
|
||||||
|
for (int i=0; i<channels; i++) {
|
||||||
|
if (bps == 8) {
|
||||||
|
uint8_t l = sample[i] & 0xff;
|
||||||
|
f.write(&l, sizeof(l));
|
||||||
|
} else {
|
||||||
|
uint8_t l = sample[i] & 0xff;
|
||||||
|
uint8_t h = (sample[i] >> 8) & 0xff;
|
||||||
|
f.write(&l, sizeof(l));
|
||||||
|
f.write(&h, sizeof(h));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool AudioOutputSPIFFSWAV::stop()
|
||||||
|
{
|
||||||
|
uint8_t wavHeader[sizeof(wavHeaderTemplate)];
|
||||||
|
|
||||||
|
memcpy_P(wavHeader, wavHeaderTemplate, sizeof(wavHeaderTemplate));
|
||||||
|
|
||||||
|
int chunksize = f.size() - 8;
|
||||||
|
wavHeader[4] = chunksize & 0xff;
|
||||||
|
wavHeader[5] = (chunksize>>8)&0xff;
|
||||||
|
wavHeader[6] = (chunksize>>16)&0xff;
|
||||||
|
wavHeader[7] = (chunksize>>24)&0xff;
|
||||||
|
|
||||||
|
wavHeader[22] = channels & 0xff;
|
||||||
|
wavHeader[23] = 0;
|
||||||
|
|
||||||
|
wavHeader[24] = hertz & 0xff;
|
||||||
|
wavHeader[25] = (hertz >> 8) & 0xff;
|
||||||
|
wavHeader[26] = (hertz >> 16) & 0xff;
|
||||||
|
wavHeader[27] = (hertz >> 24) & 0xff;
|
||||||
|
int byteRate = hertz * bps * channels / 8;
|
||||||
|
wavHeader[28] = byteRate & 0xff;
|
||||||
|
wavHeader[29] = (byteRate >> 8) & 0xff;
|
||||||
|
wavHeader[30] = (byteRate >> 16) & 0xff;
|
||||||
|
wavHeader[31] = (byteRate >> 24) & 0xff;
|
||||||
|
wavHeader[32] = channels * bps / 8;
|
||||||
|
wavHeader[33] = 0;
|
||||||
|
wavHeader[34] = bps;
|
||||||
|
wavHeader[35] = 0;
|
||||||
|
|
||||||
|
int datasize = f.size() - sizeof(wavHeader);
|
||||||
|
wavHeader[40] = datasize & 0xff;
|
||||||
|
wavHeader[41] = (datasize>>8)&0xff;
|
||||||
|
wavHeader[42] = (datasize>>16)&0xff;
|
||||||
|
wavHeader[43] = (datasize>>24)&0xff;
|
||||||
|
|
||||||
|
// Write real header out
|
||||||
|
f.seek(0, SeekSet);
|
||||||
|
f.write(wavHeader, sizeof(wavHeader));
|
||||||
|
f.close();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
AudioOutputSPIFFSWAV
|
||||||
|
Writes a WAV file to the SPIFFS filesystem
|
||||||
|
|
||||||
|
Copyright (C) 2017 Earle F. Philhower, III
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _AUDIOOUTPUTSPIFFSWAV_H
|
||||||
|
#define _AUDIOOUTPUTSPIFFSWAV_H
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <FS.h>
|
||||||
|
|
||||||
|
#include "AudioOutput.h"
|
||||||
|
|
||||||
|
class AudioOutputSPIFFSWAV : public AudioOutput
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
AudioOutputSPIFFSWAV() { filename = NULL; };
|
||||||
|
~AudioOutputSPIFFSWAV() { free(filename); };
|
||||||
|
virtual bool begin() override;
|
||||||
|
virtual bool ConsumeSample(int16_t sample[2]) override;
|
||||||
|
virtual bool stop() override;
|
||||||
|
void SetFilename(const char *name);
|
||||||
|
|
||||||
|
private:
|
||||||
|
File f;
|
||||||
|
char *filename;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue