Переглянути джерело

Add microphone recording to MP3 on a /audio page

The board's two microphones are analog MEMS capsules feeding an ES7210
ADC, not PDM, so they need an I2C register init before I2S produces
anything but silence. There is no Arduino driver for the part; the
sequence in audio.cpp is transcribed from ESPHome's es7210 component,
which follows esp-adf, along with the single 16 kHz coefficient row.

Audio is encoded to MP3 as it is captured, at 16 kHz and 32 kbps. Raw
PCM is 32 kB/s, eight times the size for no useful gain on microphones
of this class. The ESP32-S3 has no hardware encoder, so shine is
vendored under lib/ alongside ST7305_U8g2 and does it in software.

Three destinations, chosen per recording: PSRAM with a download when
finished, a file on the SD card, or a live stream played in the browser.
Mono by default with both microphones mixed; stereo keeps the array.

Capture and encoding run in their own task on core 1, so the clock keeps
updating throughout. The live stream is the reason for the care: writing
from the WebServer handler would freeze the panel for the whole
recording, so the handler attaches the socket and returns and
audio::loop() drains into it in bounded chunks. That relies on
WebServer releasing its client by assignment rather than stop(), with
the descriptor reference counted, which was checked against the
installed core.

Shine cannot encode in real time at 80 MHz, so the CPU is raised to
240 MHz for the duration of a recording and restored afterwards, and
dark-room sleep is suppressed while one is running. The microphones stay
unpowered until a recording actually starts, which is both a power and a
privacy decision.

platformio.ini is pinned to espressif32@6.5.0: the capture path uses the
legacy driver/i2s.h API that arduino-esp32 3.x removed.

None of this is verified on hardware. CLAUDE.md carries an ordered list
of what to suspect first, and notes that shine is LGPL-2.1 — the only
copyleft dependency in the tree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Benjamin Harris 1 місяць тому
батько
коміт
f5911fe75a

+ 75 - 2
CLAUDE.md

@@ -34,6 +34,17 @@ Confirmed from the vendor examples in
 | KEY / BOOT buttons | 18 / 0, both active low |
 | Battery divider | ADC1_CH3 = GPIO4, ×3, downstream of the charger |
 | BH1750 (optional, user-fitted) | 0x23 or 0x5C on the same I²C bus |
+| I²S MCLK / BCLK / LRCLK | 16 / 9 / 45 |
+| ES7210 mic ADC | 0x40, data in on GPIO10 |
+| ES8311 speaker DAC | 0x18, data out on GPIO8 |
+| Speaker amplifier enable | 46, held low — playback is not built |
+| microSD (SDMMC, 1-bit) | CLK 38, CMD 21, D0 39 |
+
+**The two microphones are not PDM.** They are analog MEMS capsules into an
+ES7210 ADC, so they need an I²C register init before I²S produces anything but
+silence. The audio pins come from `07_Audio_Test` and are corroborated by
+Waveshare's own ESPHome example for this board; both were checked because a
+wrong pin here fails silently as a stream of zeros.
 
 **The panel has no backlight or frontlight and there is no onboard light
 sensor.** Both were checked against the vendor docs and examples. This is why
@@ -55,8 +66,9 @@ octal PSRAM. Dropping it silently disables PSRAM at boot.
 ## Layout
 
 `lib/ST7305_U8g2/` is vendored verbatim from the vendor's `10_U8G2_Test`
-example (Apache-2.0). Treat it as third party: fix bugs upstream-style, keep
-local changes minimal and commented.
+example (Apache-2.0). `lib/shine/` is vendored verbatim from toots/shine
+(**LGPL-2.1** — the one copyleft dependency here; see its README). Treat both as
+third party: fix bugs upstream-style, keep local changes minimal and commented.
 
 ```text
 src/main.cpp          startup order + the loop; owns the indoor reading
@@ -70,6 +82,8 @@ src/portal.{h,cpp}    setup web UI; the HTML/CSS/JS page lives here
 src/battery.{h,cpp}   18650 voltage and percentage
 src/ambient.{h,cpp}   BH1750, with quiet-hours fallback
 src/power.{h,cpp}     CPU clock, panel mode, dark-room light sleep
+src/audio.{h,cpp}     ES7210 mics, I2S capture, MP3 encoding, destinations
+src/audio_portal.{h,cpp}  the /audio page and its API
 include/config.h      pins, layout, intervals, compiled-in defaults
 ```
 
@@ -126,6 +140,33 @@ with Wi-Fi active, and at or above 80 MHz the APB clock stays 80 MHz so the
 display SPI and UART dividers are unaffected. `power::begin()` must run before
 `Serial.begin()` or the log comes out garbled.
 
+**Recording never touches the clock.** Capture and encoding run in a FreeRTOS
+task pinned to core 1, so `loop()` keeps updating the panel throughout. This is
+also why the live-stream destination does not write from its HTTP handler: the
+handler attaches the socket and returns, and `audio::loop()` drains the encoder
+into it a bounded chunk at a time. A blocking `while (recording) { write(); }`
+inside a `WebServer` handler would freeze the clock for the whole recording,
+which is the one thing this firmware must not do.
+
+**The stream handler relies on `WiFiClient` being reference counted.**
+`WebServer::handleClient()` releases its client with `_currentClient =
+WiFiClient()` rather than `.stop()`, and the socket descriptor lives in a
+`shared_ptr`, so the copy `audio::attachStreamClient()` keeps holds the
+connection open after the handler returns. This was verified against the
+installed core, not assumed — if the core is ever upgraded, check it again.
+
+**80 MHz is abandoned while recording, deliberately.** Shine cannot encode in
+real time at 80 MHz, so `audio::start()` raises the CPU to 240 MHz and the
+teardown puts it back. The raise happens before `i2s_driver_install()` so the
+I²S dividers are computed at the final clock, and the restore happens on the
+main task in `finishIfDone()` so it cannot race the encoder. Dark-room sleep is
+suppressed for the duration.
+
+**The microphones are unpowered unless a recording is running.** `audio::begin()`
+only probes for the ES7210 and mounts the card; the codec is configured and the
+I²S driver installed in `start()` and torn down again in `finishIfDone()`. Keep
+it that way — it is both a power decision and a privacy one.
+
 **The portal page is one self-contained document** in `portal.cpp` — no CDN,
 no external fonts. In setup mode the phone loading it has no route to the
 internet, so any external reference would simply hang. There is a build-time
@@ -159,12 +200,44 @@ board. Check these first if something misbehaves:
 - **BH1750 timing.** A flat 180 ms one-shot delay, the datasheet maximum, with
   no busy polling.
 
+The whole audio path is also unverified, and is the largest block of untested
+code in the repository. Check in this order if recording misbehaves:
+
+- **The ES7210 register sequence** in `audio.cpp` is transcribed from ESPHome's
+  `es7210` component, which follows esp-adf. The magic numbers are reproduced,
+  not derived — the datasheet does not explain most of them. Only the 16 kHz
+  coefficient row is copied, because that is the only rate this firmware
+  records at; another rate needs the matching row from upstream's table.
+- **Silence** points at the codec init or the I²S pins, not the encoder. A
+  valid MP3 of pure zeros is the expected failure mode for a wrong `data_in`
+  pin, since the I²S peripheral happily clocks in nothing.
+- **Whether shine keeps up at 240 MHz.** Never measured on hardware. If it does
+  not, the symptom is I²S overruns and choppy audio; drop `AUDIO_BITRATE_KBPS`
+  or force mono before anything else.
+- **`SD_MMC` sharing GPIO38/21/39.** Nothing else in the firmware claims those
+  pins, but the card has never been exercised alongside the display SPI.
+
+`AUDIO_MIC_GAIN` is a guess at a sensible default (21 dB). It is a compile-time
+constant, not a portal setting, because there is no hardware to calibrate it
+against yet.
+
 ## Not implemented
 
 Everything under README "Future Features", plus partial-region panel updates
 and the TE pin. Deep sleep is not used anywhere — light sleep retains RAM,
 which is what keeps the last outdoor reading across a dark night.
 
+**Audio playback.** The ES8311 and the speaker are wired and their pins are in
+`config.h`, but nothing drives them and `SPEAKER_PA_PIN` is held low. Recording
+is capture-only. Nothing about audio appears on the panel either — no recording
+indicator, no level meter — and KEY is untouched, so the button still means
+only "hold at power-up for setup".
+
+`platformio.ini` now pins `espressif32@6.5.0`. `audio.cpp` uses the legacy
+`driver/i2s.h` API, which arduino-esp32 3.x removed in favour of
+`driver/i2s_std.h`. Unpinning without rewriting the capture path will not
+compile.
+
 No runtime current measurement has been taken, so avoid quoting runtime
 figures in the docs.
 

+ 97 - 3
README.md

@@ -54,6 +54,8 @@ The board provides:
 * 300 × 400 pixel resolution
 * PCF85063 real-time clock
 * SHTC3 temperature/humidity sensor
+* Two MEMS microphones on an ES7210 ADC
+* Speaker driven by an ES8311 DAC
 * TF card interface
 * USB Type-C
 * 18650 battery holder
@@ -386,20 +388,29 @@ A suggested project structure:
 │   ├── ambient.cpp
 │   ├── ambient.h
 │   ├── power.cpp
-│   └── power.h
+│   ├── power.h
+│   ├── audio.cpp
+│   ├── audio.h
+│   ├── audio_portal.cpp
+│   └── audio_portal.h
 │
 ├── include/
 │   ├── config.h
 │   └── i2c_bus.h
 │
 └── lib/
-    └── ST7305_U8g2/
+    ├── ST7305_U8g2/
+    └── shine/
 ```
 
 `settings` holds the runtime configuration in flash; `portal` serves the setup
 web page. Fonts are compiled in from the U8g2 library, so no font files are
 shipped.
 
+`audio` owns the microphone codec and the MP3 encoder; `audio_portal` serves
+the `/audio` page. They are kept apart from `portal` because the two pages have
+nothing to say to each other.
+
 ## Configuration
 
 All user-specific settings are entered on the display itself through a setup
@@ -544,6 +555,76 @@ it to quarters also means normal voltage noise causes no screen refresh. Below
 
 The setup page shows the exact voltage and percentage.
 
+## Audio Recording
+
+The board carries two MEMS microphones feeding an **ES7210** ADC. They are not
+part of the clock's job, so they are kept entirely out of the display: nothing
+about recording appears on the panel, and the microphones stay unpowered until
+a recording actually starts.
+
+Recording is driven from a second web page at `http://temp-display.local/audio`,
+linked from the bottom of the settings page.
+
+### Format
+
+Audio is encoded to **MP3 as it is captured**, at 16 kHz and 32 kbps — speech
+grade, about 4 kB per second. Raw 16-bit PCM from the ADC is 32 kB/s, which is
+eight times the size for no useful gain on microphones of this class.
+
+The ESP32-S3 has no hardware audio encoder, so the encoding is done in software
+by the fixed-point [shine](https://github.com/toots/shine) library. This cannot
+keep up at the 80 MHz the clock normally runs at, so **the CPU is raised to
+240 MHz for the duration of a recording and dropped straight back afterwards.**
+Recording is therefore not a low-power activity, and it suppresses dark-room
+sleep while it runs.
+
+### Channels
+
+| Mode | Result |
+| --- | --- |
+| Mono (default) | MIC1 and MIC2 mixed to one channel, 32 kbps |
+| Stereo | MIC1 left, MIC2 right, 64 kbps |
+
+Mono is the default because it halves both the file size and the encoder load,
+and two microphones 20 mm apart add little for a voice note. Stereo is there for
+when the two-microphone array itself is the point.
+
+### Destinations
+
+| Destination | Behaviour |
+| --- | --- |
+| Memory | Encoded into PSRAM and downloaded when you stop. Capped at 5 MB, roughly 20 minutes of mono. Lost on reboot |
+| SD card | Written to the TF card as `/<name>.mp3`. Limited only by free space, and survives a reboot. Recordings are listed on the page for download or deletion |
+| Live stream | Sent to the browser as it is encoded and played there. Nothing is kept |
+
+The page shows the elapsed time, the size so far and a live input level meter
+from both microphones, so it is obvious whether the microphones are actually
+hearing anything.
+
+### The clock keeps running
+
+Capture and encoding run in their own task on the second core. The clock, the
+indoor reading and the panel refresh all continue at their normal rates
+throughout a recording, including while audio is streaming to a browser — the
+streaming is fed from a buffer in bounded pieces rather than being allowed to
+block the main loop.
+
+### The speaker is not used
+
+The board's ES8311 DAC and speaker are wired and identified, but no playback is
+implemented. The amplifier enable line is deliberately held low, both to save
+current and to keep the speaker from feeding back into the microphones during a
+recording.
+
+### Privacy
+
+The web interface is plain HTTP with no authentication, so **anyone on the same
+network can start a recording and listen to it.** That was already true of the
+settings page; it matters more here. The microphones are only powered while a
+recording is running and nothing is captured at any other time, but this is
+worth understanding before the device is put in a room where it matters. On an
+untrusted network, this page should not be exposed.
+
 ## Future Features
 
 Potential future additions:
@@ -588,4 +669,17 @@ Official documentation:
 
 ## Licence
 
-This project is released under the terms specified by the repository owner.
+This project is released under the terms specified by the repository owner.
+
+Two third-party libraries are vendored under `lib/`:
+
+| Library | Licence | Origin |
+| --- | --- | --- |
+| `ST7305_U8g2` | Apache-2.0 | WaveShare's `10_U8G2_Test` example |
+| `shine` | LGPL-2.1 | [toots/shine](https://github.com/toots/shine) |
+
+Shine's LGPL is more restrictive than anything else here. Statically linked into
+a firmware image, its relinking obligation is only really satisfiable by
+publishing the source or the object files — not a problem for a personal device
+whose source is already public, but worth knowing before this firmware is
+handed out as a binary.

+ 48 - 0
include/config.h

@@ -111,6 +111,54 @@
 #define OUTDOOR_STALE_MS          (10UL * 60UL * 1000UL)  // mark reading stale
 #define DISPLAY_TICK_MS           1000UL    // check-for-change rate
 
+// ----------------------------------------------------------------- audio ---
+// The board carries two analog MEMS microphones feeding an ES7210 ADC, and an
+// ES8311 DAC driving the speaker, on one shared I2S bus. Only the capture half
+// is used here. Pins are from the vendor's 07_Audio_Test and match Waveshare's
+// own ESPHome example for this board.
+#define I2S_MCLK_PIN       16
+#define I2S_BCLK_PIN       9
+#define I2S_LRCLK_PIN      45
+#define ES7210_DIN_PIN     10   // ADC -> ESP32, the microphones
+#define ES8311_DOUT_PIN    8    // ESP32 -> DAC, unused: playback is not built
+#define SPEAKER_PA_PIN     46   // amplifier enable, held low so we stay silent
+
+#define ES7210_ADDR        0x40
+#define ES8311_ADDR        0x18
+
+// 16 kHz mono at 32 kbps is speech-grade and about 4 kB/s. The ES7210 always
+// hands over two channels; MIC1 and MIC2 are averaged when recording mono.
+#define AUDIO_SAMPLE_RATE  16000
+#define AUDIO_BITRATE_KBPS 32
+#define AUDIO_STEREO_DEFAULT false
+
+// ES7210 analog gain, 0-14 in 3 dB steps. 7 is ~21 dB: enough for a voice at
+// desk distance without clipping a hand clap.
+#define AUDIO_MIC_GAIN     7
+
+// MP3 encoding is far too slow at CPU_FREQ_MHZ, so the clock is raised for the
+// duration of a recording and dropped back the moment it ends.
+#define AUDIO_CPU_FREQ_MHZ 240
+
+// PSRAM recordings stop at this size rather than filling the heap. At 32 kbps
+// mono, 5 MB is a little over 20 minutes.
+#define AUDIO_PSRAM_MAX_BYTES  (5UL * 1024UL * 1024UL)
+
+// MP3 frames waiting to be written to their destination. One 16 kHz mono frame
+// is 576 samples, so this is about 4 seconds of slack for an SD write or a
+// stalled TCP connection.
+#define AUDIO_RING_BYTES   (32UL * 1024UL)
+
+// Bytes pushed to a streaming client per loop() pass. Small enough that the
+// clock never waits long on the network, large enough to stay ahead of a
+// 4 kB/s encoder.
+#define AUDIO_STREAM_CHUNK 1460
+
+// microSD, wired for SDMMC in 1-bit mode per the vendor's 06_SD_Card.
+#define SD_CLK_PIN         38
+#define SD_CMD_PIN         21
+#define SD_D0_PIN          39
+
 // ---------------------------------------------------------------- outdoor ---
 // HTTP timeout for one outdoor poll.
 #define OUTDOOR_HTTP_TIMEOUT_MS   5000

+ 482 - 0
lib/shine/COPYING

@@ -0,0 +1,482 @@
+		  GNU LIBRARY GENERAL PUBLIC LICENSE
+		       Version 2, June 1991
+
+ Copyright (C) 1991 Free Software Foundation, Inc.
+		    51 Franklin St, Fifth Floor, Boston, MA  02110-1301, USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the library GPL.  It is
+ numbered 2 because it goes with version 2 of the ordinary GPL.]
+
+			    Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+  This license, the Library General Public License, applies to some
+specially designated Free Software Foundation software, and to any
+other libraries whose authors decide to use it.  You can use it for
+your libraries, 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
+this service 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 make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if
+you distribute copies of the library, or if you modify it.
+
+  For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you.  You must make sure that they, too, receive or can get the source
+code.  If you link a program with the library, you must provide
+complete object files to the recipients so that they can relink them
+with the library, after making changes to the library and recompiling
+it.  And you must show them these terms so they know their rights.
+
+  Our method of protecting your rights has two steps: (1) copyright
+the library, and (2) offer you this license which gives you legal
+permission to copy, distribute and/or modify the library.
+
+  Also, for each distributor's protection, we want to make certain
+that everyone understands that there is no warranty for this free
+library.  If the library is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original
+version, so that any problems introduced by others will not reflect on
+the original authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that companies distributing free
+software will individually obtain patent licenses, thus in effect
+transforming the program into proprietary software.  To prevent this,
+we have made it clear that any patent must be licensed for everyone's
+free use or not licensed at all.
+
+  Most GNU software, including some libraries, is covered by the ordinary
+GNU General Public License, which was designed for utility programs.  This
+license, the GNU Library General Public License, applies to certain
+designated libraries.  This license is quite different from the ordinary
+one; be sure to read it in full, and don't assume that anything in it is
+the same as in the ordinary license.
+
+  The reason we have a separate public license for some libraries is that
+they blur the distinction we usually make between modifying or adding to a
+program and simply using it.  Linking a program with a library, without
+changing the library, is in some sense simply using the library, and is
+analogous to running a utility program or application program.  However, in
+a textual and legal sense, the linked executable is a combined work, a
+derivative of the original library, and the ordinary General Public License
+treats it as such.
+
+  Because of this blurred distinction, using the ordinary General
+Public License for libraries did not effectively promote software
+sharing, because most developers did not use the libraries.  We
+concluded that weaker conditions might promote sharing better.
+
+  However, unrestricted linking of non-free programs would deprive the
+users of those programs of all benefit from the free status of the
+libraries themselves.  This Library General Public License is intended to
+permit developers of non-free programs to use free libraries, while
+preserving your freedom as a user of such programs to change the free
+libraries that are incorporated in them.  (We have not seen how to achieve
+this as regards changes in header files, but we have achieved it as regards
+changes in the actual functions of the Library.)  The hope is that this
+will lead to faster development of free libraries.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.  Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library".  The
+former contains code derived from the library, while the latter only
+works together with the library.
+
+  Note that it is possible for a library to be covered by the ordinary
+General Public License rather than by this special one.
+
+		  GNU LIBRARY GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License Agreement applies to any software library which
+contains a notice placed by the copyright holder or other authorized
+party saying it may be distributed under the terms of this Library
+General Public License (also called "this License").  Each licensee is
+addressed as "you".
+
+  A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+  The "Library", below, refers to any such software library or work
+which has been distributed under these terms.  A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language.  (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+  "Source code" for a work means the preferred form of the work for
+making modifications to it.  For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+  Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it).  Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+  
+  1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+  You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+  2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) The modified work must itself be a software library.
+
+    b) You must cause the files modified to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    c) You must cause the whole of the work to be licensed at no
+    charge to all third parties under the terms of this License.
+
+    d) If a facility in the modified Library refers to a function or a
+    table of data to be supplied by an application program that uses
+    the facility, other than as an argument passed when the facility
+    is invoked, then you must make a good faith effort to ensure that,
+    in the event an application does not supply such function or
+    table, the facility still operates, and performs whatever part of
+    its purpose remains meaningful.
+
+    (For example, a function in a library to compute square roots has
+    a purpose that is entirely well-defined independent of the
+    application.  Therefore, Subsection 2d requires that any
+    application-supplied function or table used by this function must
+    be optional: if the application does not supply it, the square
+    root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library.  To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License.  (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.)  Do not make any other change in
+these notices.
+
+  Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+  This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+  4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+  If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library".  Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+  However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library".  The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+  When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library.  The
+threshold for this to be true is not precisely defined by law.
+
+  If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work.  (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+  Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+  6. As an exception to the Sections above, you may also compile or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+  You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License.  You must supply a copy of this License.  If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License.  Also, you must do one
+of these things:
+
+    a) Accompany the work with the complete corresponding
+    machine-readable source code for the Library including whatever
+    changes were used in the work (which must be distributed under
+    Sections 1 and 2 above); and, if the work is an executable linked
+    with the Library, with the complete machine-readable "work that
+    uses the Library", as object code and/or source code, so that the
+    user can modify the Library and then relink to produce a modified
+    executable containing the modified Library.  (It is understood
+    that the user who changes the contents of definitions files in the
+    Library will not necessarily be able to recompile the application
+    to use the modified definitions.)
+
+    b) Accompany the work with a written offer, valid for at
+    least three years, to give the same user the materials
+    specified in Subsection 6a, above, for a charge no more
+    than the cost of performing this distribution.
+
+    c) If distribution of the work is made by offering access to copy
+    from a designated place, offer equivalent access to copy the above
+    specified materials from the same place.
+
+    d) Verify that the user has already received a copy of these
+    materials or that you have already sent this user a copy.
+
+  For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it.  However, as a special exception,
+the source code distributed need not include anything that is normally
+distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+  It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system.  Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+  7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+    a) Accompany the combined library with a copy of the same work
+    based on the Library, uncombined with any other library
+    facilities.  This must be distributed under the terms of the
+    Sections above.
+
+    b) Give prominent notice with the combined library of the fact
+    that part of it is a work based on the Library, and explaining
+    where to find the accompanying uncombined form of the same work.
+
+  8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License.  Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License.  However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+  9. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Library or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+  10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded.  In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+  13. The Free Software Foundation may publish revised and/or new
+versions of the Library General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation.  If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+  14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission.  For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this.  Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+			    NO WARRANTY
+
+  15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU.  SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+		     END OF TERMS AND CONDITIONS
+
+           How to Apply These Terms to Your New Libraries
+
+  If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change.  You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+  To apply these terms, attach the following notices to the library.  It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the library's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public
+    License along with this library; if not, write to the 
+    Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+    Boston, MA  02110-1301, USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the
+  library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+  <signature of Ty Coon>, 1 April 1990
+  Ty Coon, President of Vice
+
+That's all there is to it!

+ 37 - 0
lib/shine/README.md

@@ -0,0 +1,37 @@
+# shine — fixed-point MP3 encoder
+
+Vendored verbatim from [toots/shine](https://github.com/toots/shine) at commit
+`ab5e3526` (2023-01-01). See `UPSTREAM_VERSION.txt`.
+
+Treat this as third party, the same way `lib/ST7305_U8g2/` is treated: fix bugs
+upstream-style and keep local changes minimal and commented. **There are
+currently no local changes** — `src/` is a straight copy of upstream's
+`src/lib/`.
+
+## Why it is here
+
+The ESP32-S3 has no hardware audio encoder, and raw 16-bit PCM from the ES7210
+is 32 kB/s — about four minutes into the whole PSRAM heap. Shine is fixed-point
+and skips psychoacoustic analysis entirely, which is what makes real-time
+encoding possible on this part at all. It is the encoder the ESP32 audio
+community has settled on for exactly this reason.
+
+Only the parts of upstream needed to build the library are copied: the autotools
+scripts, the JS/Android bindings and the command-line front end are all left
+behind. Shine needs no generated `config.h`, so it compiles as-is.
+
+## Sample rates
+
+Upstream supports MPEG-2 LSF, so **16 kHz is a valid sample rate** (`tables.c`
+lists 16000 under MPEG-II). Some older forks are MPEG-1 only, which is where the
+widely repeated "shine cannot go below 32 kHz" claim comes from. It does not
+apply to this copy.
+
+## Licence
+
+Shine is **LGPL-2.1**, which is more restrictive than everything else in this
+repository. Statically linked into a firmware image, the LGPL's relinking
+obligation is only really satisfiable by publishing the object files or the
+source. That is not a problem for a personal device whose source is already
+public, but it is worth knowing before this firmware is distributed as a binary.
+`COPYING` is upstream's licence text, unmodified.

+ 1 - 0
lib/shine/UPSTREAM_VERSION.txt

@@ -0,0 +1 @@
+ab5e3526b64af1a2eaa43aa6f441a7312e013519 2023-01-01

+ 17 - 0
lib/shine/library.json

@@ -0,0 +1,17 @@
+{
+  "name": "shine",
+  "version": "1.0.0",
+  "description": "Shine fixed-point MPEG Layer III (MP3) encoder. Vendored verbatim from toots/shine (LGPL-2.1), commit ab5e3526. Used to compress the ES7210 microphone capture on the fly — the ESP32-S3 has no hardware audio encoder.",
+  "license": "LGPL-2.1",
+  "frameworks": "arduino",
+  "platforms": "espressif32",
+  "build": {
+    "srcDir": "src",
+    "includeDir": "src",
+    "flags": [
+      "-O2",
+      "-Wno-unused-but-set-variable",
+      "-Wno-unused-variable"
+    ]
+  }
+}

+ 76 - 0
lib/shine/src/bitstream.c

@@ -0,0 +1,76 @@
+/*
+ *  bit_stream.c package
+ *  Author:  Jean-Georges Fritsch, C-Cube Microsystems
+ *
+ * This package provides functions to write information to the bit stream.
+ *
+ * Removed unused functions. Feb 2001 P.Everett
+ */
+
+#include "bitstream.h"
+#include "types.h"
+
+#if !defined(__APPLE__) && !defined(__FreeBSD__)
+#include <malloc.h>
+#endif
+
+/* open the device to write the bit stream into it */
+void shine_open_bit_stream(bitstream_t *bs, int size) {
+  bs->data = (unsigned char *)malloc(size * sizeof(unsigned char));
+  bs->data_size = size;
+  bs->data_position = 0;
+  bs->cache = 0;
+  bs->cache_bits = 32;
+}
+
+/*close the device containing the bit stream */
+void shine_close_bit_stream(bitstream_t *bs) {
+  if (bs->data)
+    free(bs->data);
+}
+
+/*
+ * shine_putbits:
+ * --------
+ * write N bits into the bit stream.
+ * bs = bit stream structure
+ * val = value to write into the buffer
+ * N = number of bits of val
+ */
+void shine_putbits(bitstream_t *bs, unsigned int val, unsigned int N) {
+#ifdef DEBUG
+  if (N > 32)
+    printf("Cannot write more than 32 bits at a time.\n");
+  if (N < 32 && (val >> N) != 0)
+    printf("Upper bits (higher than %d) are not all zeros.\n", N);
+#endif
+
+  if (bs->cache_bits > N) {
+    bs->cache_bits -= N;
+    bs->cache |= val << bs->cache_bits;
+  } else {
+    if (bs->data_position + sizeof(unsigned int) >= bs->data_size) {
+      bs->data = (unsigned char *)realloc(bs->data,
+                                          bs->data_size + (bs->data_size / 2));
+      bs->data_size += (bs->data_size / 2);
+    }
+
+    N -= bs->cache_bits;
+    bs->cache |= val >> N;
+#ifdef SHINE_BIG_ENDIAN
+    *(unsigned int *)(bs->data + bs->data_position) = bs->cache;
+#else
+    *(unsigned int *)(bs->data + bs->data_position) = SWAB32(bs->cache);
+#endif
+    bs->data_position += sizeof(unsigned int);
+    bs->cache_bits = 32 - N;
+    if (N != 0)
+      bs->cache = val << bs->cache_bits;
+    else
+      bs->cache = 0;
+  }
+}
+
+int shine_get_bits_count(bitstream_t *bs) {
+  return bs->data_position * 8 + 32 - bs->cache_bits;
+}

+ 29 - 0
lib/shine/src/bitstream.h

@@ -0,0 +1,29 @@
+#ifndef BITSTREAM_H
+#define BITSTREAM_H
+
+typedef struct bit_stream_struc {
+  unsigned char *data; /* Processed data */
+  int data_size;       /* Total data size */
+  int data_position;   /* Data position */
+  unsigned int cache;  /* bit stream cache */
+  int cache_bits;      /* free bits in cache */
+} bitstream_t;
+
+/* "bit_stream.h" Definitions */
+
+#define MINIMUM 4 /* Minimum size of the buffer in bytes */
+#define MAX_LENGTH                                                             \
+  32 /* Maximum length of word written or                                      \
+        read from bit stream */
+
+#define BUFFER_SIZE 4096
+
+#define MIN(A, B) ((A) < (B) ? (A) : (B))
+#define MAX(A, B) ((A) > (B) ? (A) : (B))
+
+void shine_open_bit_stream(bitstream_t *bs, const int size);
+void shine_close_bit_stream(bitstream_t *bs);
+void shine_putbits(bitstream_t *bs, unsigned int val, unsigned int N);
+int shine_get_bits_count(bitstream_t *bs);
+
+#endif

+ 248 - 0
lib/shine/src/huffman.c

@@ -0,0 +1,248 @@
+/* huffman.c */
+
+#include "huffman.h"
+#include "bitstream.h"
+#include "l3bitstream.h"
+#include "types.h"
+
+const HUFFBITS dmask = 1 << (((sizeof(HUFFBITS)) << 3) - 1);
+const unsigned int hs = sizeof(HUFFBITS) << 3;
+
+static const HUFFBITS t1HB[] = {1, 1, 1, 0};
+static const HUFFBITS t2HB[] = {1, 2, 1, 3, 1, 1, 3, 2, 0};
+static const HUFFBITS t3HB[] = {3, 2, 1, 1, 1, 1, 3, 2, 0};
+static const HUFFBITS t5HB[] = {1, 2, 6, 5, 3, 1, 4, 4, 7, 5, 7, 1, 6, 1, 1, 0};
+static const HUFFBITS t6HB[] = {7, 3, 5, 1, 6, 2, 3, 2, 5, 4, 4, 1, 3, 3, 2, 0};
+static const HUFFBITS t7HB[] = {1,  2, 10, 19, 16, 10, 3,  3,  7,  10, 5,  3,
+                                11, 4, 13, 17, 8,  4,  12, 11, 18, 15, 11, 2,
+                                7,  6, 9,  14, 3,  1,  6,  4,  5,  3,  2,  0};
+static const HUFFBITS t8HB[] = {3,  4, 6, 18, 12, 5, 5,  1,  2,  16, 9,  3,
+                                7,  3, 5, 14, 7,  3, 19, 17, 15, 13, 10, 4,
+                                13, 5, 8, 11, 5,  1, 12, 4,  4,  1,  1,  0};
+static const HUFFBITS t9HB[] = {7,  5, 9, 14, 15, 7, 6,  4, 5, 5,  6, 7,
+                                7,  6, 8, 8,  8,  5, 15, 6, 9, 10, 5, 1,
+                                11, 7, 9, 6,  4,  1, 14, 4, 6, 2,  6, 0};
+static const HUFFBITS t10HB[] = {
+    1,  2,  10, 23, 35, 30, 12, 17, 3,  3,  8,  12, 18, 21, 12, 7,
+    11, 9,  15, 21, 32, 40, 19, 6,  14, 13, 22, 34, 46, 23, 18, 7,
+    20, 19, 33, 47, 27, 22, 9,  3,  31, 22, 41, 26, 21, 20, 5,  3,
+    14, 13, 10, 11, 16, 6,  5,  1,  9,  8,  7,  8,  4,  4,  2,  0};
+static const HUFFBITS t11HB[] = {
+    3,  4,  10, 24, 34, 33, 21, 15, 5,  3,  4,  10, 32, 17, 11, 10,
+    11, 7,  13, 18, 30, 31, 20, 5,  25, 11, 19, 59, 27, 18, 12, 5,
+    35, 33, 31, 58, 30, 16, 7,  5,  28, 26, 32, 19, 17, 15, 8,  14,
+    14, 12, 9,  13, 14, 9,  4,  1,  11, 4,  6,  6,  6,  3,  2,  0};
+static const HUFFBITS t12HB[] = {
+    9,  6,  16, 33, 41, 39, 38, 26, 7,  5,  6,  9,  23, 16, 26, 11,
+    17, 7,  11, 14, 21, 30, 10, 7,  17, 10, 15, 12, 18, 28, 14, 5,
+    32, 13, 22, 19, 18, 16, 9,  5,  40, 17, 31, 29, 17, 13, 4,  2,
+    27, 12, 11, 15, 10, 7,  4,  1,  27, 12, 8,  12, 6,  3,  1,  0};
+static const HUFFBITS t13HB[] = {
+    1,  5,  14, 21, 34, 51, 46, 71, 42, 52, 68,  52, 67, 44, 43, 19, 3,   4,
+    12, 19, 31, 26, 44, 33, 31, 24, 32, 24, 31,  35, 22, 14, 15, 13, 23,  36,
+    59, 49, 77, 65, 29, 40, 30, 40, 27, 33, 42,  16, 22, 20, 37, 61, 56,  79,
+    73, 64, 43, 76, 56, 37, 26, 31, 25, 14, 35,  16, 60, 57, 97, 75, 114, 91,
+    54, 73, 55, 41, 48, 53, 23, 24, 58, 27, 50,  96, 76, 70, 93, 84, 77,  58,
+    79, 29, 74, 49, 41, 17, 47, 45, 78, 74, 115, 94, 90, 79, 69, 83, 71,  50,
+    59, 38, 36, 15, 72, 34, 56, 95, 92, 85, 91,  90, 86, 73, 77, 65, 51,  44,
+    43, 42, 43, 20, 30, 44, 55, 78, 72, 87, 78,  61, 46, 54, 37, 30, 20,  16,
+    53, 25, 41, 37, 44, 59, 54, 81, 66, 76, 57,  54, 37, 18, 39, 11, 35,  33,
+    31, 57, 42, 82, 72, 80, 47, 58, 55, 21, 22,  26, 38, 22, 53, 25, 23,  38,
+    70, 60, 51, 36, 55, 26, 34, 23, 27, 14, 9,   7,  34, 32, 28, 39, 49,  75,
+    30, 52, 48, 40, 52, 28, 18, 17, 9,  5,  45,  21, 34, 64, 56, 50, 49,  45,
+    31, 19, 12, 15, 10, 7,  6,  3,  48, 23, 20,  39, 36, 35, 53, 21, 16,  23,
+    13, 10, 6,  1,  4,  2,  16, 15, 17, 27, 25,  20, 29, 11, 17, 12, 16,  8,
+    1,  1,  0,  1};
+static const HUFFBITS t15HB[] = {
+    7,   12, 18, 53, 47, 76, 124, 108, 89, 123, 108, 119, 107, 81, 122, 63,
+    13,  5,  16, 27, 46, 36, 61,  51,  42, 70,  52,  83,  65,  41, 59,  36,
+    19,  17, 15, 24, 41, 34, 59,  48,  40, 64,  50,  78,  62,  80, 56,  33,
+    29,  28, 25, 43, 39, 63, 55,  93,  76, 59,  93,  72,  54,  75, 50,  29,
+    52,  22, 42, 40, 67, 57, 95,  79,  72, 57,  89,  69,  49,  66, 46,  27,
+    77,  37, 35, 66, 58, 52, 91,  74,  62, 48,  79,  63,  90,  62, 40,  38,
+    125, 32, 60, 56, 50, 92, 78,  65,  55, 87,  71,  51,  73,  51, 70,  30,
+    109, 53, 49, 94, 88, 75, 66,  122, 91, 73,  56,  42,  64,  44, 21,  25,
+    90,  43, 41, 77, 73, 63, 56,  92,  77, 66,  47,  67,  48,  53, 36,  20,
+    71,  34, 67, 60, 58, 49, 88,  76,  67, 106, 71,  54,  38,  39, 23,  15,
+    109, 53, 51, 47, 90, 82, 58,  57,  48, 72,  57,  41,  23,  27, 62,  9,
+    86,  42, 40, 37, 70, 64, 52,  43,  70, 55,  42,  25,  29,  18, 11,  11,
+    118, 68, 30, 55, 50, 46, 74,  65,  49, 39,  24,  16,  22,  13, 14,  7,
+    91,  44, 39, 38, 34, 63, 52,  45,  31, 52,  28,  19,  14,  8,  9,   3,
+    123, 60, 58, 53, 47, 43, 32,  22,  37, 24,  17,  12,  15,  10, 2,   1,
+    71,  37, 34, 30, 28, 20, 17,  26,  21, 16,  10,  6,   8,   6,  2,   0};
+static const HUFFBITS t16HB[] = {
+    1,   5,   14,  44,  74,  63,  110, 93,   172, 149, 138,  242,  225,  195,
+    376, 17,  3,   4,   12,  20,  35,  62,   53,  47,  83,   75,   68,   119,
+    201, 107, 207, 9,   15,  13,  23,  38,   67,  58,  103,  90,   161,  72,
+    127, 117, 110, 209, 206, 16,  45,  21,   39,  69,  64,   114,  99,   87,
+    158, 140, 252, 212, 199, 387, 365, 26,   75,  36,  68,   65,   115,  101,
+    179, 164, 155, 264, 246, 226, 395, 382,  362, 9,   66,   30,   59,   56,
+    102, 185, 173, 265, 142, 253, 232, 400,  388, 378, 445,  16,   111,  54,
+    52,  100, 184, 178, 160, 133, 257, 244,  228, 217, 385,  366,  715,  10,
+    98,  48,  91,  88,  165, 157, 148, 261,  248, 407, 397,  372,  380,  889,
+    884, 8,   85,  84,  81,  159, 156, 143,  260, 249, 427,  401,  392,  383,
+    727, 713, 708, 7,   154, 76,  73,  141,  131, 256, 245,  426,  406,  394,
+    384, 735, 359, 710, 352, 11,  139, 129,  67,  125, 247,  233,  229,  219,
+    393, 743, 737, 720, 885, 882, 439, 4,    243, 120, 118,  115,  227,  223,
+    396, 746, 742, 736, 721, 712, 706, 223,  436, 6,   202,  224,  222,  218,
+    216, 389, 386, 381, 364, 888, 443, 707,  440, 437, 1728, 4,    747,  211,
+    210, 208, 370, 379, 734, 723, 714, 1735, 883, 877, 876,  3459, 865,  2,
+    377, 369, 102, 187, 726, 722, 358, 711,  709, 866, 1734, 871,  3458, 870,
+    434, 0,   12,  10,  7,   11,  10,  17,   11,  9,   13,   12,   10,   7,
+    5,   3,   1,   3};
+static const HUFFBITS t24HB[] = {
+    15,  13,  46,  80,  146, 262, 248, 434, 426, 669, 653, 649, 621, 517, 1032,
+    88,  14,  12,  21,  38,  71,  130, 122, 216, 209, 198, 327, 345, 319, 297,
+    279, 42,  47,  22,  41,  74,  68,  128, 120, 221, 207, 194, 182, 340, 315,
+    295, 541, 18,  81,  39,  75,  70,  134, 125, 116, 220, 204, 190, 178, 325,
+    311, 293, 271, 16,  147, 72,  69,  135, 127, 118, 112, 210, 200, 188, 352,
+    323, 306, 285, 540, 14,  263, 66,  129, 126, 119, 114, 214, 202, 192, 180,
+    341, 317, 301, 281, 262, 12,  249, 123, 121, 117, 113, 215, 206, 195, 185,
+    347, 330, 308, 291, 272, 520, 10,  435, 115, 111, 109, 211, 203, 196, 187,
+    353, 332, 313, 298, 283, 531, 381, 17,  427, 212, 208, 205, 201, 193, 186,
+    177, 169, 320, 303, 286, 268, 514, 377, 16,  335, 199, 197, 191, 189, 181,
+    174, 333, 321, 305, 289, 275, 521, 379, 371, 11,  668, 184, 183, 179, 175,
+    344, 331, 314, 304, 290, 277, 530, 383, 373, 366, 10,  652, 346, 171, 168,
+    164, 318, 309, 299, 287, 276, 263, 513, 375, 368, 362, 6,   648, 322, 316,
+    312, 307, 302, 292, 284, 269, 261, 512, 376, 370, 364, 359, 4,   620, 300,
+    296, 294, 288, 282, 273, 266, 515, 380, 374, 369, 365, 361, 357, 2,   1033,
+    280, 278, 274, 267, 264, 259, 382, 378, 372, 367, 363, 360, 358, 356, 0,
+    43,  20,  19,  17,  15,  13,  11,  9,   7,   6,   4,   7,   5,   3,   1,
+    3};
+static const HUFFBITS t32HB[] = {1, 5, 4, 5, 6, 5, 4, 4,
+                                 7, 3, 6, 0, 7, 2, 3, 1};
+static const HUFFBITS t33HB[] = {15, 14, 13, 12, 11, 10, 9, 8,
+                                 7,  6,  5,  4,  3,  2,  1, 0};
+
+static const unsigned char t1l[] = {1, 3, 2, 3};
+static const unsigned char t2l[] = {1, 3, 6, 3, 3, 5, 5, 5, 6};
+static const unsigned char t3l[] = {2, 2, 6, 3, 2, 5, 5, 5, 6};
+static const unsigned char t5l[] = {1, 3, 6, 7, 3, 3, 6, 7,
+                                    6, 6, 7, 8, 7, 6, 7, 8};
+static const unsigned char t6l[] = {3, 3, 5, 7, 3, 2, 4, 5,
+                                    4, 4, 5, 6, 6, 5, 6, 7};
+static const unsigned char t7l[] = {1, 3, 6, 8, 8, 9,  3, 4, 6, 7,  7,  8,
+                                    6, 5, 7, 8, 8, 9,  7, 7, 8, 9,  9,  9,
+                                    7, 7, 8, 9, 9, 10, 8, 8, 9, 10, 10, 10};
+static const unsigned char t8l[] = {2, 3, 6, 8, 8,  9,  3, 2, 4, 8, 8,  8,
+                                    6, 4, 6, 8, 8,  9,  8, 8, 8, 9, 9,  10,
+                                    8, 7, 8, 9, 10, 10, 9, 8, 9, 9, 11, 11};
+static const unsigned char t9l[] = {3, 3, 5, 6, 8, 9, 3, 3, 4, 5, 6, 8,
+                                    4, 4, 5, 6, 7, 8, 6, 5, 6, 7, 7, 8,
+                                    7, 6, 7, 7, 8, 9, 8, 7, 8, 8, 9, 9};
+static const unsigned char t10l[] = {
+    1, 3, 6, 8,  9,  9,  9,  10, 3, 4, 6,  7,  8,  9,  8,  8,
+    6, 6, 7, 8,  9,  10, 9,  9,  7, 7, 8,  9,  10, 10, 9,  10,
+    8, 8, 9, 10, 10, 10, 10, 10, 9, 9, 10, 10, 11, 11, 10, 11,
+    8, 8, 9, 10, 10, 10, 11, 11, 9, 8, 9,  10, 10, 11, 11, 11};
+static const unsigned char t11l[] = {
+    2, 3, 5, 7, 8, 9,  8,  9,  3, 3, 4, 6,  8,  8,  7,  8,
+    5, 5, 6, 7, 8, 9,  8,  8,  7, 6, 7, 9,  8,  10, 8,  9,
+    8, 8, 8, 9, 9, 10, 9,  10, 8, 8, 9, 10, 10, 11, 10, 11,
+    8, 7, 7, 8, 9, 10, 10, 10, 8, 7, 8, 9,  10, 10, 10, 10};
+static const unsigned char t12l[] = {
+    4, 3, 5, 7, 8, 9, 9, 9, 3, 3, 4, 5,  7, 7, 8, 8, 5, 4, 5, 6, 7, 8,
+    7, 8, 6, 5, 6, 6, 7, 8, 8, 8, 7, 6,  7, 7, 8, 8, 8, 9, 8, 7, 8, 8,
+    8, 9, 8, 9, 8, 7, 7, 8, 8, 9, 9, 10, 9, 8, 8, 9, 9, 9, 9, 10};
+static const unsigned char t13l[] = {
+    1,  4,  6,  7,  8,  9,  9,  10, 9,  10, 11, 11, 12, 12, 13, 13, 3,  4,  6,
+    7,  8,  8,  9,  9,  9,  9,  10, 10, 11, 12, 12, 12, 6,  6,  7,  8,  9,  9,
+    10, 10, 9,  10, 10, 11, 11, 12, 13, 13, 7,  7,  8,  9,  9,  10, 10, 10, 10,
+    11, 11, 11, 11, 12, 13, 13, 8,  7,  9,  9,  10, 10, 11, 11, 10, 11, 11, 12,
+    12, 13, 13, 14, 9,  8,  9,  10, 10, 10, 11, 11, 11, 11, 12, 11, 13, 13, 14,
+    14, 9,  9,  10, 10, 11, 11, 11, 11, 11, 12, 12, 12, 13, 13, 14, 14, 10, 9,
+    10, 11, 11, 11, 12, 12, 12, 12, 13, 13, 13, 14, 16, 16, 9,  8,  9,  10, 10,
+    11, 11, 12, 12, 12, 12, 13, 13, 14, 15, 15, 10, 9,  10, 10, 11, 11, 11, 13,
+    12, 13, 13, 14, 14, 14, 16, 15, 10, 10, 10, 11, 11, 12, 12, 13, 12, 13, 14,
+    13, 14, 15, 16, 17, 11, 10, 10, 11, 12, 12, 12, 12, 13, 13, 13, 14, 15, 15,
+    15, 16, 11, 11, 11, 12, 12, 13, 12, 13, 14, 14, 15, 15, 15, 16, 16, 16, 12,
+    11, 12, 13, 13, 13, 14, 14, 14, 14, 14, 15, 16, 15, 16, 16, 13, 12, 12, 13,
+    13, 13, 15, 14, 14, 17, 15, 15, 15, 17, 16, 16, 12, 12, 13, 14, 14, 14, 15,
+    14, 15, 15, 16, 16, 19, 18, 19, 16};
+static const unsigned char t15l[] = {
+    3,  4,  5,  7,  7,  8,  9,  9,  9,  10, 10, 11, 11, 11, 12, 13, 4,  3,  5,
+    6,  7,  7,  8,  8,  8,  9,  9,  10, 10, 10, 11, 11, 5,  5,  5,  6,  7,  7,
+    8,  8,  8,  9,  9,  10, 10, 11, 11, 11, 6,  6,  6,  7,  7,  8,  8,  9,  9,
+    9,  10, 10, 10, 11, 11, 11, 7,  6,  7,  7,  8,  8,  9,  9,  9,  9,  10, 10,
+    10, 11, 11, 11, 8,  7,  7,  8,  8,  8,  9,  9,  9,  9,  10, 10, 11, 11, 11,
+    12, 9,  7,  8,  8,  8,  9,  9,  9,  9,  10, 10, 10, 11, 11, 12, 12, 9,  8,
+    8,  9,  9,  9,  9,  10, 10, 10, 10, 10, 11, 11, 11, 12, 9,  8,  8,  9,  9,
+    9,  9,  10, 10, 10, 10, 11, 11, 12, 12, 12, 9,  8,  9,  9,  9,  9,  10, 10,
+    10, 11, 11, 11, 11, 12, 12, 12, 10, 9,  9,  9,  10, 10, 10, 10, 10, 11, 11,
+    11, 11, 12, 13, 12, 10, 9,  9,  9,  10, 10, 10, 10, 11, 11, 11, 11, 12, 12,
+    12, 13, 11, 10, 9,  10, 10, 10, 11, 11, 11, 11, 11, 11, 12, 12, 13, 13, 11,
+    10, 10, 10, 10, 11, 11, 11, 11, 12, 12, 12, 12, 12, 13, 13, 12, 11, 11, 11,
+    11, 11, 11, 11, 12, 12, 12, 12, 13, 13, 12, 13, 12, 11, 11, 11, 11, 11, 11,
+    12, 12, 12, 12, 12, 13, 13, 13, 13};
+static const unsigned char t16l[] = {
+    1,  4,  6,  8,  9,  9,  10, 10, 11, 11, 11, 12, 12, 12, 13, 9,  3,  4,  6,
+    7,  8,  9,  9,  9,  10, 10, 10, 11, 12, 11, 12, 8,  6,  6,  7,  8,  9,  9,
+    10, 10, 11, 10, 11, 11, 11, 12, 12, 9,  8,  7,  8,  9,  9,  10, 10, 10, 11,
+    11, 12, 12, 12, 13, 13, 10, 9,  8,  9,  9,  10, 10, 11, 11, 11, 12, 12, 12,
+    13, 13, 13, 9,  9,  8,  9,  9,  10, 11, 11, 12, 11, 12, 12, 13, 13, 13, 14,
+    10, 10, 9,  9,  10, 11, 11, 11, 11, 12, 12, 12, 12, 13, 13, 14, 10, 10, 9,
+    10, 10, 11, 11, 11, 12, 12, 13, 13, 13, 13, 15, 15, 10, 10, 10, 10, 11, 11,
+    11, 12, 12, 13, 13, 13, 13, 14, 14, 14, 10, 11, 10, 10, 11, 11, 12, 12, 13,
+    13, 13, 13, 14, 13, 14, 13, 11, 11, 11, 10, 11, 12, 12, 12, 12, 13, 14, 14,
+    14, 15, 15, 14, 10, 12, 11, 11, 11, 12, 12, 13, 14, 14, 14, 14, 14, 14, 13,
+    14, 11, 12, 12, 12, 12, 12, 13, 13, 13, 13, 15, 14, 14, 14, 14, 16, 11, 14,
+    12, 12, 12, 13, 13, 14, 14, 14, 16, 15, 15, 15, 17, 15, 11, 13, 13, 11, 12,
+    14, 14, 13, 14, 14, 15, 16, 15, 17, 15, 14, 11, 9,  8,  8,  9,  9,  10, 10,
+    10, 11, 11, 11, 11, 11, 11, 11, 8};
+static const unsigned char t24l[] = {
+    4,  4,  6,  7,  8,  9,  9,  10, 10, 11, 11, 11, 11, 11, 12, 9,  4,  4,  5,
+    6,  7,  8,  8,  9,  9,  9,  10, 10, 10, 10, 10, 8,  6,  5,  6,  7,  7,  8,
+    8,  9,  9,  9,  9,  10, 10, 10, 11, 7,  7,  6,  7,  7,  8,  8,  8,  9,  9,
+    9,  9,  10, 10, 10, 10, 7,  8,  7,  7,  8,  8,  8,  8,  9,  9,  9,  10, 10,
+    10, 10, 11, 7,  9,  7,  8,  8,  8,  8,  9,  9,  9,  9,  10, 10, 10, 10, 10,
+    7,  9,  8,  8,  8,  8,  9,  9,  9,  9,  10, 10, 10, 10, 10, 11, 7,  10, 8,
+    8,  8,  9,  9,  9,  9,  10, 10, 10, 10, 10, 11, 11, 8,  10, 9,  9,  9,  9,
+    9,  9,  9,  9,  10, 10, 10, 10, 11, 11, 8,  10, 9,  9,  9,  9,  9,  9,  10,
+    10, 10, 10, 10, 11, 11, 11, 8,  11, 9,  9,  9,  9,  10, 10, 10, 10, 10, 10,
+    11, 11, 11, 11, 8,  11, 10, 9,  9,  9,  10, 10, 10, 10, 10, 10, 11, 11, 11,
+    11, 8,  11, 10, 10, 10, 10, 10, 10, 10, 10, 10, 11, 11, 11, 11, 11, 8,  11,
+    10, 10, 10, 10, 10, 10, 10, 11, 11, 11, 11, 11, 11, 11, 8,  12, 10, 10, 10,
+    10, 10, 10, 11, 11, 11, 11, 11, 11, 11, 11, 8,  8,  7,  7,  7,  7,  7,  7,
+    7,  7,  7,  7,  8,  8,  8,  8,  4};
+static const unsigned char t32l[] = {1, 4, 4, 5, 4, 6, 5, 6,
+                                     4, 5, 5, 6, 5, 6, 6, 6};
+static const unsigned char t33l[] = {4, 4, 4, 4, 4, 4, 4, 4,
+                                     4, 4, 4, 4, 4, 4, 4, 4};
+
+#define NOREF -1
+const struct huffcodetab shine_huffman_table[HTN] = {
+    {0, 0, 0, 0, NULL, NULL},
+    {2, 2, 0, 0, t1HB, t1l},
+    {3, 3, 0, 0, t2HB, t2l},
+    {3, 3, 0, 0, t3HB, t3l},
+    {0, 0, 0, 0, NULL, NULL}, /* Apparently not used*/
+    {4, 4, 0, 0, t5HB, t5l},
+    {4, 4, 0, 0, t6HB, t6l},
+    {6, 6, 0, 0, t7HB, t7l},
+    {6, 6, 0, 0, t8HB, t8l},
+    {6, 6, 0, 0, t9HB, t9l},
+    {8, 8, 0, 0, t10HB, t10l},
+    {8, 8, 0, 0, t11HB, t11l},
+    {8, 8, 0, 0, t12HB, t12l},
+    {16, 16, 0, 0, t13HB, t13l},
+    {0, 0, 0, 0, NULL, NULL}, /* Apparently not used*/
+    {16, 16, 0, 0, t15HB, t15l},
+    {16, 16, 1, 1, t16HB, t16l},
+    {16, 16, 2, 3, t16HB, t16l},
+    {16, 16, 3, 7, t16HB, t16l},
+    {16, 16, 4, 15, t16HB, t16l},
+    {16, 16, 6, 63, t16HB, t16l},
+    {16, 16, 8, 255, t16HB, t16l},
+    {16, 16, 10, 1023, t16HB, t16l},
+    {16, 16, 13, 8191, t16HB, t16l},
+    {16, 16, 4, 15, t24HB, t24l},
+    {16, 16, 5, 31, t24HB, t24l},
+    {16, 16, 6, 63, t24HB, t24l},
+    {16, 16, 7, 127, t24HB, t24l},
+    {16, 16, 8, 255, t24HB, t24l},
+    {16, 16, 9, 511, t24HB, t24l},
+    {16, 16, 11, 2047, t24HB, t24l},
+    {16, 16, 13, 8191, t24HB, t24l},
+    {1, 16, 0, 0, t32HB, t32l},
+    {1, 16, 0, 0, t33HB, t33l},
+};

+ 20 - 0
lib/shine/src/huffman.h

@@ -0,0 +1,20 @@
+#include <stdint.h>
+
+#define HUFFBITS uint16_t
+#define HTN 34
+#define MXOFF 250
+
+struct huffcodetab {
+  unsigned int xlen;         /*max. x-index+                         */
+  unsigned int ylen;         /*max. y-index+                         */
+  unsigned int linbits;      /*number of linbits                     */
+  unsigned int linmax;       /*max number to be stored in linbits    */
+  const HUFFBITS *table;     /*pointer to array[xlen][ylen]          */
+  const unsigned char *hlen; /*pointer to array[xlen][ylen]          */
+};
+
+extern const struct huffcodetab
+    shine_huffman_table[HTN]; /* global memory block                */
+                              /* array of all huffcodtable headers    */
+                              /* 0..31 Huffman code table 0..31       */
+                              /* 32,33 count1-tables                  */

+ 309 - 0
lib/shine/src/l3bitstream.c

@@ -0,0 +1,309 @@
+/* l3bitstrea.c */
+
+#include "l3bitstream.h" /* the public interface */
+#include "bitstream.h"
+#include "huffman.h"
+#include "l3loop.h"
+#include "l3mdct.h"
+#include "layer3.h"
+#include "tables.h"
+#include "types.h"
+
+static void shine_HuffmanCode(bitstream_t *bs, int table_select, int x, int y);
+static void shine_huffman_coder_count1(bitstream_t *bs,
+                                       const struct huffcodetab *h, int v,
+                                       int w, int x, int y);
+
+static void encodeSideInfo(shine_global_config *config);
+static void encodeMainData(shine_global_config *config);
+static void Huffmancodebits(shine_global_config *config, int *ix, gr_info *gi);
+
+/*
+  shine_format_bitstream()
+
+  This is called after a frame of audio has been quantized and coded.
+  It will write the encoded audio to the bitstream. Note that
+  from a layer3 encoder's perspective the bit stream is primarily
+  a series of main_data() blocks, with header and side information
+  inserted at the proper locations to maintain framing. (See Figure A.7
+  in the IS).
+*/
+
+void shine_format_bitstream(shine_global_config *config) {
+  int gr, ch, i;
+
+  for (ch = 0; ch < config->wave.channels; ch++)
+    for (gr = 0; gr < config->mpeg.granules_per_frame; gr++) {
+      int *pi = &config->l3_enc[ch][gr][0];
+      int32_t *pr = &config->mdct_freq[ch][gr][0];
+      for (i = 0; i < GRANULE_SIZE; i++) {
+        if ((pr[i] < 0) && (pi[i] > 0))
+          pi[i] *= -1;
+      }
+    }
+
+  encodeSideInfo(config);
+  encodeMainData(config);
+}
+
+static void encodeMainData(shine_global_config *config) {
+  int gr, ch, sfb;
+  shine_side_info_t si = config->side_info;
+
+  for (gr = 0; gr < config->mpeg.granules_per_frame; gr++) {
+    for (ch = 0; ch < config->wave.channels; ch++) {
+      gr_info *gi = &(si.gr[gr].ch[ch].tt);
+      unsigned slen1 = shine_slen1_tab[gi->scalefac_compress];
+      unsigned slen2 = shine_slen2_tab[gi->scalefac_compress];
+      int *ix = &config->l3_enc[ch][gr][0];
+
+      if (gr == 0 || si.scfsi[ch][0] == 0)
+        for (sfb = 0; sfb < 6; sfb++)
+          shine_putbits(&config->bs, config->scalefactor.l[gr][ch][sfb], slen1);
+      if (gr == 0 || si.scfsi[ch][1] == 0)
+        for (sfb = 6; sfb < 11; sfb++)
+          shine_putbits(&config->bs, config->scalefactor.l[gr][ch][sfb], slen1);
+      if (gr == 0 || si.scfsi[ch][2] == 0)
+        for (sfb = 11; sfb < 16; sfb++)
+          shine_putbits(&config->bs, config->scalefactor.l[gr][ch][sfb], slen2);
+      if (gr == 0 || si.scfsi[ch][3] == 0)
+        for (sfb = 16; sfb < 21; sfb++)
+          shine_putbits(&config->bs, config->scalefactor.l[gr][ch][sfb], slen2);
+
+      Huffmancodebits(config, ix, gi);
+    }
+  }
+}
+
+static void encodeSideInfo(shine_global_config *config) {
+  int gr, ch, scfsi_band, region;
+  shine_side_info_t si = config->side_info;
+
+  shine_putbits(&config->bs, 0x7ff, 11);
+  shine_putbits(&config->bs, config->mpeg.version, 2);
+  shine_putbits(&config->bs, config->mpeg.layer, 2);
+  shine_putbits(&config->bs, !config->mpeg.crc, 1);
+  shine_putbits(&config->bs, config->mpeg.bitrate_index, 4);
+  shine_putbits(&config->bs, config->mpeg.samplerate_index % 3, 2);
+  shine_putbits(&config->bs, config->mpeg.padding, 1);
+  shine_putbits(&config->bs, config->mpeg.ext, 1);
+  shine_putbits(&config->bs, config->mpeg.mode, 2);
+  shine_putbits(&config->bs, config->mpeg.mode_ext, 2);
+  shine_putbits(&config->bs, config->mpeg.copyright, 1);
+  shine_putbits(&config->bs, config->mpeg.original, 1);
+  shine_putbits(&config->bs, config->mpeg.emph, 2);
+
+  if (config->mpeg.version == MPEG_I) {
+    shine_putbits(&config->bs, 0, 9);
+    if (config->wave.channels == 2)
+      shine_putbits(&config->bs, si.private_bits, 3);
+    else
+      shine_putbits(&config->bs, si.private_bits, 5);
+  } else {
+    shine_putbits(&config->bs, 0, 8);
+    if (config->wave.channels == 2)
+      shine_putbits(&config->bs, si.private_bits, 2);
+    else
+      shine_putbits(&config->bs, si.private_bits, 1);
+  }
+
+  if (config->mpeg.version == MPEG_I)
+    for (ch = 0; ch < config->wave.channels; ch++) {
+      for (scfsi_band = 0; scfsi_band < 4; scfsi_band++)
+        shine_putbits(&config->bs, si.scfsi[ch][scfsi_band], 1);
+    }
+
+  for (gr = 0; gr < config->mpeg.granules_per_frame; gr++)
+    for (ch = 0; ch < config->wave.channels; ch++) {
+      gr_info *gi = &(si.gr[gr].ch[ch].tt);
+
+      shine_putbits(&config->bs, gi->part2_3_length, 12);
+      shine_putbits(&config->bs, gi->big_values, 9);
+      shine_putbits(&config->bs, gi->global_gain, 8);
+      if (config->mpeg.version == MPEG_I)
+        shine_putbits(&config->bs, gi->scalefac_compress, 4);
+      else
+        shine_putbits(&config->bs, gi->scalefac_compress, 9);
+      shine_putbits(&config->bs, 0, 1);
+
+      for (region = 0; region < 3; region++)
+        shine_putbits(&config->bs, gi->table_select[region], 5);
+
+      shine_putbits(&config->bs, gi->region0_count, 4);
+      shine_putbits(&config->bs, gi->region1_count, 3);
+
+      if (config->mpeg.version == MPEG_I)
+        shine_putbits(&config->bs, gi->preflag, 1);
+      shine_putbits(&config->bs, gi->scalefac_scale, 1);
+      shine_putbits(&config->bs, gi->count1table_select, 1);
+    }
+}
+
+/* Note the discussion of huffmancodebits() on pages 28 and 29 of the IS, as
+  well as the definitions of the side information on pages 26 and 27. */
+static void Huffmancodebits(shine_global_config *config, int *ix, gr_info *gi) {
+  const int *scalefac =
+      &shine_scale_fact_band_index[config->mpeg.samplerate_index][0];
+  unsigned scalefac_index;
+  int region1Start, region2Start;
+  int i, bigvalues, count1End;
+  int v, w, x, y;
+  const struct huffcodetab *h;
+  int bits;
+
+  bits = shine_get_bits_count(&config->bs);
+
+  /* 1: Write the bigvalues */
+  bigvalues = gi->big_values << 1;
+
+  scalefac_index = gi->region0_count + 1;
+  region1Start = scalefac[scalefac_index];
+  scalefac_index += gi->region1_count + 1;
+  region2Start = scalefac[scalefac_index];
+
+  for (i = 0; i < bigvalues; i += 2) {
+    /* get table pointer */
+    int idx = (i >= region1Start) + (i >= region2Start);
+    unsigned tableindex = gi->table_select[idx];
+    /* get huffman code */
+    if (tableindex) {
+      x = ix[i];
+      y = ix[i + 1];
+      shine_HuffmanCode(&config->bs, tableindex, x, y);
+    }
+  }
+
+  /* 2: Write count1 area */
+  h = &shine_huffman_table[gi->count1table_select + 32];
+  count1End = bigvalues + (gi->count1 << 2);
+  for (i = bigvalues; i < count1End; i += 4) {
+    v = ix[i];
+    w = ix[i + 1];
+    x = ix[i + 2];
+    y = ix[i + 3];
+    shine_huffman_coder_count1(&config->bs, h, v, w, x, y);
+  }
+
+  bits = shine_get_bits_count(&config->bs) - bits;
+  bits = gi->part2_3_length - gi->part2_length - bits;
+  if (bits) {
+    int stuffingWords = bits / 32;
+    int remainingBits = bits % 32;
+
+    /* Due to the nature of the Huffman code tables, we will pad with ones */
+    while (stuffingWords--)
+      shine_putbits(&config->bs, ~0, 32);
+    if (remainingBits)
+      shine_putbits(&config->bs, (1UL << remainingBits) - 1, remainingBits);
+  }
+}
+
+static inline int shine_abs_and_sign(int *x) {
+  if (*x > 0)
+    return 0;
+  *x *= -1;
+  return 1;
+}
+
+static void shine_huffman_coder_count1(bitstream_t *bs,
+                                       const struct huffcodetab *h, int v,
+                                       int w, int x, int y) {
+  unsigned int signv, signw, signx, signy;
+  unsigned int code = 0;
+  int p, cbits = 0;
+
+  signv = shine_abs_and_sign(&v);
+  signw = shine_abs_and_sign(&w);
+  signx = shine_abs_and_sign(&x);
+  signy = shine_abs_and_sign(&y);
+
+  p = v + (w << 1) + (x << 2) + (y << 3);
+  shine_putbits(bs, h->table[p], h->hlen[p]);
+
+  if (v) {
+    code = signv;
+    cbits = 1;
+  }
+  if (w) {
+    code = (code << 1) | signw;
+    cbits++;
+  }
+  if (x) {
+    code = (code << 1) | signx;
+    cbits++;
+  }
+  if (y) {
+    code = (code << 1) | signy;
+    cbits++;
+  }
+  shine_putbits(bs, code, cbits);
+}
+
+/* Implements the pseudocode of page 98 of the IS */
+static void shine_HuffmanCode(bitstream_t *bs, int table_select, int x, int y) {
+  int cbits = 0, xbits = 0;
+  unsigned int code = 0, ext = 0;
+  unsigned signx, signy, ylen, idx;
+  const struct huffcodetab *h;
+
+  signx = shine_abs_and_sign(&x);
+  signy = shine_abs_and_sign(&y);
+
+  h = &(shine_huffman_table[table_select]);
+  ylen = h->ylen;
+
+  if (table_select > 15) { /* ESC-table is used */
+    unsigned linbitsx = 0, linbitsy = 0, linbits = h->linbits;
+
+    if (x > 14) {
+      linbitsx = x - 15;
+      x = 15;
+    }
+    if (y > 14) {
+      linbitsy = y - 15;
+      y = 15;
+    }
+
+    idx = (x * ylen) + y;
+    code = h->table[idx];
+    cbits = h->hlen[idx];
+    if (x > 14) {
+      ext |= linbitsx;
+      xbits += linbits;
+    }
+    if (x != 0) {
+      ext <<= 1;
+      ext |= signx;
+      xbits += 1;
+    }
+    if (y > 14) {
+      ext <<= linbits;
+      ext |= linbitsy;
+      xbits += linbits;
+    }
+    if (y != 0) {
+      ext <<= 1;
+      ext |= signy;
+      xbits += 1;
+    }
+
+    shine_putbits(bs, code, cbits);
+    shine_putbits(bs, ext, xbits);
+  } else { /* No ESC-words */
+    idx = (x * ylen) + y;
+    code = h->table[idx];
+    cbits = h->hlen[idx];
+    if (x != 0) {
+      code <<= 1;
+      code |= signx;
+      cbits += 1;
+    }
+    if (y != 0) {
+      code <<= 1;
+      code |= signy;
+      cbits += 1;
+    }
+
+    shine_putbits(bs, code, cbits);
+  }
+}

+ 8 - 0
lib/shine/src/l3bitstream.h

@@ -0,0 +1,8 @@
+#ifndef shine_BITSTREAM_H
+#define shine_BITSTREAM_H
+
+#include "types.h"
+
+void shine_format_bitstream(shine_global_config *config);
+
+#endif

+ 803 - 0
lib/shine/src/l3loop.c

@@ -0,0 +1,803 @@
+/* l3loop.c */
+
+#include "l3loop.h"
+#include "bitstream.h"
+#include "huffman.h"
+#include "l3bitstream.h"
+#include "layer3.h"
+#include "reservoir.h"
+#include "tables.h"
+#include "types.h"
+
+#define e 2.71828182845
+#define CBLIMIT 21
+#define SFB_LMAX 22
+#define en_tot_krit 10
+#define en_dif_krit 100
+#define en_scfsi_band_krit 10
+#define xm_scfsi_band_krit 10
+
+static void calc_scfsi(shine_psy_xmin_t *l3_xmin, int ch, int gr,
+                       shine_global_config *config);
+static int part2_length(int gr, int ch, shine_global_config *config);
+static int bin_search_StepSize(int desired_rate, int ix[GRANULE_SIZE],
+                               gr_info *cod_info, shine_global_config *config);
+static int count_bit(int ix[GRANULE_SIZE], unsigned int start, unsigned int end,
+                     unsigned int table);
+static int bigv_bitcount(int ix[GRANULE_SIZE], gr_info *gi);
+static int new_choose_table(int ix[GRANULE_SIZE], unsigned int begin,
+                            unsigned int end);
+static void bigv_tab_select(int ix[GRANULE_SIZE], gr_info *cod_info);
+static void subdivide(gr_info *cod_info, shine_global_config *config);
+static int count1_bitcount(int ix[GRANULE_SIZE], gr_info *cod_info);
+static void calc_runlen(int ix[GRANULE_SIZE], gr_info *cod_info);
+static void calc_xmin(shine_psy_ratio_t *ratio, gr_info *cod_info,
+                      shine_psy_xmin_t *l3_xmin, int gr, int ch);
+static int quantize(int ix[GRANULE_SIZE], int stepsize,
+                    shine_global_config *config);
+
+/*
+ * shine_inner_loop:
+ * ----------
+ * The code selects the best quantizerStepSize for a particular set
+ * of scalefacs.
+ */
+int shine_inner_loop(int ix[GRANULE_SIZE], int max_bits, gr_info *cod_info,
+                     int gr, int ch, shine_global_config *config) {
+  int bits, c1bits, bvbits;
+
+  if (max_bits < 0)
+    cod_info->quantizerStepSize--;
+  do {
+    while (quantize(ix, ++cod_info->quantizerStepSize, config) > 8192)
+      ; /* within table range? */
+
+    calc_runlen(ix, cod_info);                     /* rzero,count1,big_values*/
+    bits = c1bits = count1_bitcount(ix, cod_info); /* count1_table selection*/
+    subdivide(cod_info, config);                   /* bigvalues sfb division */
+    bigv_tab_select(ix, cod_info);                 /* codebook selection*/
+    bits += bvbits = bigv_bitcount(ix, cod_info);  /* bit count */
+  } while (bits > max_bits);
+  return bits;
+}
+
+/*
+ * shine_outer_loop:
+ * -----------
+ *  Function: The outer iteration loop controls the masking conditions
+ *  of all scalefactorbands. It computes the best scalefac and
+ *  global gain. This module calls the inner iteration loop.
+ */
+
+int shine_outer_loop(
+    int max_bits,
+    shine_psy_xmin_t *l3_xmin, /* the allowed distortion of the scalefactor */
+    int ix[GRANULE_SIZE],      /* vector of quantized values ix(0..575) */
+    int gr, int ch, shine_global_config *config) {
+  int bits, huff_bits;
+  shine_side_info_t *side_info = &config->side_info;
+  gr_info *cod_info = &side_info->gr[gr].ch[ch].tt;
+
+  cod_info->quantizerStepSize =
+      bin_search_StepSize(max_bits, ix, cod_info, config);
+
+  cod_info->part2_length = part2_length(gr, ch, config);
+  huff_bits = max_bits - cod_info->part2_length;
+
+  bits = shine_inner_loop(ix, huff_bits, cod_info, gr, ch, config);
+  cod_info->part2_3_length = cod_info->part2_length + bits;
+
+  return cod_info->part2_3_length;
+}
+
+/*
+ * shine_iteration_loop:
+ * ------------------
+ */
+void shine_iteration_loop(shine_global_config *config) {
+  shine_psy_xmin_t l3_xmin;
+  gr_info *cod_info;
+  int max_bits;
+  int ch, gr, i;
+  int *ix;
+
+  for (ch = config->wave.channels; ch--;) {
+    for (gr = 0; gr < config->mpeg.granules_per_frame; gr++) {
+      /* setup pointers */
+      ix = config->l3_enc[ch][gr];
+      config->l3loop.xr = config->mdct_freq[ch][gr];
+
+      /* Precalculate the square, abs,  and maximum,
+       * for use later on.
+       */
+      for (i = GRANULE_SIZE, config->l3loop.xrmax = 0; i--;) {
+        config->l3loop.xrsq[i] =
+            mulsr(config->l3loop.xr[i], config->l3loop.xr[i]);
+        config->l3loop.xrabs[i] = labs(config->l3loop.xr[i]);
+        if (config->l3loop.xrabs[i] > config->l3loop.xrmax)
+          config->l3loop.xrmax = config->l3loop.xrabs[i];
+      }
+
+      cod_info = (gr_info *)&(config->side_info.gr[gr].ch[ch]);
+      cod_info->sfb_lmax = SFB_LMAX - 1; /* gr_deco */
+
+      calc_xmin(&config->ratio, cod_info, &l3_xmin, gr, ch);
+
+      if (config->mpeg.version == MPEG_I)
+        calc_scfsi(&l3_xmin, ch, gr, config);
+
+      /* calculation of number of available bit( per granule ) */
+      max_bits = shine_max_reservoir_bits(&config->pe[ch][gr], config);
+
+      /* reset of iteration variables */
+      memset(config->scalefactor.l[gr][ch], 0,
+             sizeof(config->scalefactor.l[gr][ch]));
+      memset(config->scalefactor.s[gr][ch], 0,
+             sizeof(config->scalefactor.s[gr][ch]));
+
+      for (i = 4; i--;)
+        cod_info->slen[i] = 0;
+
+      cod_info->part2_3_length = 0;
+      cod_info->big_values = 0;
+      cod_info->count1 = 0;
+      cod_info->scalefac_compress = 0;
+      cod_info->table_select[0] = 0;
+      cod_info->table_select[1] = 0;
+      cod_info->table_select[2] = 0;
+      cod_info->region0_count = 0;
+      cod_info->region1_count = 0;
+      cod_info->part2_length = 0;
+      cod_info->preflag = 0;
+      cod_info->scalefac_scale = 0;
+      cod_info->count1table_select = 0;
+
+      /* all spectral values zero ? */
+      if (config->l3loop.xrmax)
+        cod_info->part2_3_length =
+            shine_outer_loop(max_bits, &l3_xmin, ix, gr, ch, config);
+
+      shine_ResvAdjust(cod_info, config);
+      cod_info->global_gain = cod_info->quantizerStepSize + 210;
+
+    } /* for gr */
+  }   /* for ch */
+
+  shine_ResvFrameEnd(config);
+}
+
+/*
+ * calc_scfsi:
+ * -----------
+ * calculation of the scalefactor select information ( scfsi ).
+ */
+void calc_scfsi(shine_psy_xmin_t *l3_xmin, int ch, int gr,
+                shine_global_config *config) {
+  shine_side_info_t *l3_side = &config->side_info;
+  /* This is the scfsi_band table from 2.4.2.7 of the IS */
+  static const int scfsi_band_long[5] = {0, 6, 11, 16, 21};
+
+  int scfsi_band;
+
+  int sfb, start, end, i;
+  int condition = 0;
+  int temp;
+
+  const int *scalefac_band_long =
+      &shine_scale_fact_band_index[config->mpeg.samplerate_index][0];
+
+  /* note. it goes quite a bit faster if you uncomment the next bit and exit
+     early from scfsi, but you then loose the advantage of common scale factors.
+
+  for(scfsi_band=0;scfsi_band<4;scfsi_band++)
+    l3_side->scfsi[ch][scfsi_band] = 0;
+  return;
+
+  */
+
+  config->l3loop.xrmaxl[gr] = config->l3loop.xrmax;
+
+  /* the total energy of the granule */
+  for (temp = 0, i = GRANULE_SIZE; i--;)
+    temp += config->l3loop.xrsq[i] >>
+            10; /* a bit of scaling to avoid overflow, (not very good) */
+  if (temp)
+    config->l3loop.en_tot[gr] =
+        log((double)temp * 4.768371584e-7) / LN2; /* 1024 / 0x7fffffff */
+  else
+    config->l3loop.en_tot[gr] = 0;
+
+  /* the energy of each scalefactor band, en */
+  /* the allowed distortion of each scalefactor band, xm */
+
+  for (sfb = 21; sfb--;) {
+    start = scalefac_band_long[sfb];
+    end = scalefac_band_long[sfb + 1];
+
+    for (temp = 0, i = start; i < end; i++)
+      temp += config->l3loop.xrsq[i] >> 10;
+    if (temp)
+      config->l3loop.en[gr][sfb] =
+          log((double)temp * 4.768371584e-7) / LN2; /* 1024 / 0x7fffffff */
+    else
+      config->l3loop.en[gr][sfb] = 0;
+
+    if (l3_xmin->l[gr][ch][sfb])
+      config->l3loop.xm[gr][sfb] = log(l3_xmin->l[gr][ch][sfb]) / LN2;
+    else
+      config->l3loop.xm[gr][sfb] = 0;
+  }
+
+  if (gr == 1) {
+    int gr2, tp;
+
+    for (gr2 = 2; gr2--;) {
+      /* The spectral values are not all zero */
+      if (config->l3loop.xrmaxl[gr2])
+        condition++;
+
+      condition++;
+    }
+    if (abs(config->l3loop.en_tot[0] - config->l3loop.en_tot[1]) < en_tot_krit)
+      condition++;
+    for (tp = 0, sfb = 21; sfb--;)
+      tp += abs(config->l3loop.en[0][sfb] - config->l3loop.en[1][sfb]);
+    if (tp < en_dif_krit)
+      condition++;
+
+    if (condition == 6) {
+      for (scfsi_band = 0; scfsi_band < 4; scfsi_band++) {
+        int sum0 = 0, sum1 = 0;
+        l3_side->scfsi[ch][scfsi_band] = 0;
+        start = scfsi_band_long[scfsi_band];
+        end = scfsi_band_long[scfsi_band + 1];
+        for (sfb = start; sfb < end; sfb++) {
+          sum0 += abs(config->l3loop.en[0][sfb] - config->l3loop.en[1][sfb]);
+          sum1 += abs(config->l3loop.xm[0][sfb] - config->l3loop.xm[1][sfb]);
+        }
+
+        if (sum0 < en_scfsi_band_krit && sum1 < xm_scfsi_band_krit) {
+          l3_side->scfsi[ch][scfsi_band] = 1;
+        } else
+          l3_side->scfsi[ch][scfsi_band] = 0;
+      } /* for scfsi_band */
+    }   /* if condition == 6 */
+    else
+      for (scfsi_band = 0; scfsi_band < 4; scfsi_band++)
+        l3_side->scfsi[ch][scfsi_band] = 0;
+  } /* if gr == 1 */
+}
+
+/*
+ * part2_length:
+ * -------------
+ * calculates the number of bits needed to encode the scalefacs in the
+ * main data block.
+ */
+int part2_length(int gr, int ch, shine_global_config *config) {
+  int slen1, slen2, bits;
+  gr_info *gi = &config->side_info.gr[gr].ch[ch].tt;
+
+  bits = 0;
+
+  {
+    slen1 = shine_slen1_tab[gi->scalefac_compress];
+    slen2 = shine_slen2_tab[gi->scalefac_compress];
+
+    if (!gr || !(config->side_info.scfsi[ch][0]))
+      bits += (6 * slen1);
+
+    if (!gr || !(config->side_info.scfsi[ch][1]))
+      bits += (5 * slen1);
+
+    if (!gr || !(config->side_info.scfsi[ch][2]))
+      bits += (5 * slen2);
+
+    if (!gr || !(config->side_info.scfsi[ch][3]))
+      bits += (5 * slen2);
+  }
+  return bits;
+}
+
+/*
+ * calc_xmin:
+ * ----------
+ * Calculate the allowed distortion for each scalefactor band,
+ * as determined by the psychoacoustic model.
+ * xmin(sb) = ratio(sb) * en(sb) / bw(sb)
+ */
+void calc_xmin(shine_psy_ratio_t *ratio, gr_info *cod_info,
+               shine_psy_xmin_t *l3_xmin, int gr, int ch) {
+  int sfb;
+
+  for (sfb = cod_info->sfb_lmax; sfb--;) {
+    /*  note. xmin will always be zero with no psychoacoustic model
+
+        start = scalefac_band_long[ sfb ];
+        end   = scalefac_band_long[ sfb+1 ];
+        bw = end - start;
+
+        for ( en = 0, l = start; l < end; l++ )
+          en += config->l3loop.xrsq[l];
+
+        l3_xmin->l[gr][ch][sfb] = ratio->l[gr][ch][sfb] * en / bw;
+    */
+    l3_xmin->l[gr][ch][sfb] = 0;
+  }
+}
+
+/*
+ * shine_loop_initialise:
+ * -------------------
+ * Calculates the look up tables used by the iteration loop.
+ */
+void shine_loop_initialise(shine_global_config *config) {
+  int i;
+
+  /* quantize: stepsize conversion, fourth root of 2 table.
+   * The table is inverted (negative power) from the equation given
+   * in the spec because it is quicker to do x*y than x/y.
+   * The 0.5 is for rounding.
+   */
+  for (i = 128; i--;) {
+    config->l3loop.steptab[i] = pow(2.0, (double)(127 - i) / 4);
+    if ((config->l3loop.steptab[i] * 2) >
+        0x7fffffff) /* MAXINT = 2**31 = 2**(124/4) */
+      config->l3loop.steptabi[i] = 0x7fffffff;
+    else
+      /* The table is multiplied by 2 to give an extra bit of accuracy.
+       * In quantize, the long multiply does not shift it's result left one
+       * bit to compensate.
+       */
+      config->l3loop.steptabi[i] =
+          (int32_t)((config->l3loop.steptab[i] * 2) + 0.5);
+  }
+
+  /* quantize: vector conversion, three quarter power table.
+   * The 0.5 is for rounding, the .0946 comes from the spec.
+   */
+  for (i = 10000; i--;)
+    config->l3loop.int2idx[i] =
+        (int)(sqrt(sqrt((double)i) * (double)i) - 0.0946 + 0.5);
+}
+
+/*
+ * quantize:
+ * ---------
+ * Function: Quantization of the vector xr ( -> ix).
+ * Returns maximum value of ix.
+ */
+int quantize(int ix[GRANULE_SIZE], int stepsize, shine_global_config *config) {
+  int i, max, ln;
+  int32_t scalei;
+  double scale, dbl;
+
+  scalei = config->l3loop.steptabi[stepsize + 127]; /* 2**(-stepsize/4) */
+
+  /* a quick check to see if ixmax will be less than 8192 */
+  /* this speeds up the early calls to bin_search_StepSize */
+  if ((mulr(config->l3loop.xrmax, scalei)) > 165140) /* 8192**(4/3) */
+    max = 16384; /* no point in continuing, stepsize not big enough */
+  else
+    for (i = 0, max = 0; i < GRANULE_SIZE; i++) {
+      /* This calculation is very sensitive. The multiply must round it's
+       * result or bad things happen to the quality.
+       */
+      ln = mulr(labs(config->l3loop.xr[i]), scalei);
+
+      if (ln < 10000)                       /* ln < 10000 catches most values */
+        ix[i] = config->l3loop.int2idx[ln]; /* quick look up method */
+      else {
+        /* outside table range so have to do it using floats */
+        scale = config->l3loop.steptab[stepsize + 127]; /* 2**(-stepsize/4) */
+        dbl = ((double)config->l3loop.xrabs[i]) * scale *
+              4.656612875e-10;              /* 0x7fffffff */
+        ix[i] = (int)sqrt(sqrt(dbl) * dbl); /* dbl**(3/4) */
+      }
+
+      /* calculate ixmax while we're here */
+      /* note. ix cannot be negative */
+      if (max < ix[i])
+        max = ix[i];
+    }
+
+  return max;
+}
+
+/*
+ * ix_max:
+ * -------
+ * Function: Calculate the maximum of ix from 0 to 575
+ */
+static inline int ix_max(int ix[GRANULE_SIZE], unsigned int begin,
+                         unsigned int end) {
+  register int i;
+  register int max = 0;
+
+  for (i = begin; i < end; i++)
+    if (max < ix[i])
+      max = ix[i];
+  return max;
+}
+
+/*
+ * calc_runlen:
+ * ------------
+ * Function: Calculation of rzero, count1, big_values
+ * (Partitions ix into big values, quadruples and zeros).
+ */
+void calc_runlen(int ix[GRANULE_SIZE], gr_info *cod_info) {
+  int i;
+  int rzero = 0;
+
+  for (i = GRANULE_SIZE; i > 1; i -= 2)
+    if (!ix[i - 1] && !ix[i - 2])
+      rzero++;
+    else
+      break;
+
+  cod_info->count1 = 0;
+  for (; i > 3; i -= 4)
+    if (ix[i - 1] <= 1 && ix[i - 2] <= 1 && ix[i - 3] <= 1 && ix[i - 4] <= 1)
+      cod_info->count1++;
+    else
+      break;
+
+  cod_info->big_values = i >> 1;
+}
+
+/*
+ * count1_bitcount:
+ * ----------------
+ * Determines the number of bits to encode the quadruples.
+ */
+int count1_bitcount(int ix[GRANULE_SIZE], gr_info *cod_info) {
+  int p, i, k;
+  int v, w, x, y, signbits;
+  int sum0 = 0, sum1 = 0;
+
+  for (i = cod_info->big_values << 1, k = 0; k < cod_info->count1;
+       i += 4, k++) {
+    v = ix[i];
+    w = ix[i + 1];
+    x = ix[i + 2];
+    y = ix[i + 3];
+
+    p = v + (w << 1) + (x << 2) + (y << 3);
+
+    signbits = 0;
+    if (v != 0)
+      signbits++;
+    if (w != 0)
+      signbits++;
+    if (x != 0)
+      signbits++;
+    if (y != 0)
+      signbits++;
+
+    sum0 += signbits;
+    sum1 += signbits;
+
+    sum0 += shine_huffman_table[32].hlen[p];
+    sum1 += shine_huffman_table[33].hlen[p];
+  }
+
+  if (sum0 < sum1) {
+    cod_info->count1table_select = 0;
+    return sum0;
+  } else {
+    cod_info->count1table_select = 1;
+    return sum1;
+  }
+}
+
+/*
+ * subdivide:
+ * ----------
+ * presumable subdivides the bigvalue region which will use separate Huffman
+ * tables.
+ */
+void subdivide(gr_info *cod_info, shine_global_config *config) {
+  static const struct {
+    unsigned region0_count;
+    unsigned region1_count;
+  } subdv_table[23] = {
+      {0, 0}, /* 0 bands */
+      {0, 0}, /* 1 bands */
+      {0, 0}, /* 2 bands */
+      {0, 0}, /* 3 bands */
+      {0, 0}, /* 4 bands */
+      {0, 1}, /* 5 bands */
+      {1, 1}, /* 6 bands */
+      {1, 1}, /* 7 bands */
+      {1, 2}, /* 8 bands */
+      {2, 2}, /* 9 bands */
+      {2, 3}, /* 10 bands */
+      {2, 3}, /* 11 bands */
+      {3, 4}, /* 12 bands */
+      {3, 4}, /* 13 bands */
+      {3, 4}, /* 14 bands */
+      {4, 5}, /* 15 bands */
+      {4, 5}, /* 16 bands */
+      {4, 6}, /* 17 bands */
+      {5, 6}, /* 18 bands */
+      {5, 6}, /* 19 bands */
+      {5, 7}, /* 20 bands */
+      {6, 7}, /* 21 bands */
+      {6, 7}, /* 22 bands */
+  };
+
+  if (!cod_info->big_values) { /* no big_values region */
+    cod_info->region0_count = 0;
+    cod_info->region1_count = 0;
+  } else {
+    const int *scalefac_band_long =
+        &shine_scale_fact_band_index[config->mpeg.samplerate_index][0];
+    int bigvalues_region, scfb_anz, thiscount;
+
+    bigvalues_region = 2 * cod_info->big_values;
+
+    /* Calculate scfb_anz */
+    scfb_anz = 0;
+    while (scalefac_band_long[scfb_anz] < bigvalues_region)
+      scfb_anz++;
+
+    for (thiscount = subdv_table[scfb_anz].region0_count; thiscount;
+         thiscount--) {
+      if (scalefac_band_long[thiscount + 1] <= bigvalues_region)
+        break;
+    }
+    cod_info->region0_count = thiscount;
+    cod_info->address1 = scalefac_band_long[thiscount + 1];
+
+    scalefac_band_long += cod_info->region0_count + 1;
+
+    for (thiscount = subdv_table[scfb_anz].region1_count; thiscount;
+         thiscount--) {
+      if (scalefac_band_long[thiscount + 1] <= bigvalues_region)
+        break;
+    }
+    cod_info->region1_count = thiscount;
+    cod_info->address2 = scalefac_band_long[thiscount + 1];
+
+    cod_info->address3 = bigvalues_region;
+  }
+}
+
+/*
+ * bigv_tab_select:
+ * ----------------
+ * Function: Select huffman code tables for bigvalues regions
+ */
+void bigv_tab_select(int ix[GRANULE_SIZE], gr_info *cod_info) {
+  cod_info->table_select[0] = 0;
+  cod_info->table_select[1] = 0;
+  cod_info->table_select[2] = 0;
+
+  {
+    if (cod_info->address1 > 0)
+      cod_info->table_select[0] = new_choose_table(ix, 0, cod_info->address1);
+
+    if (cod_info->address2 > cod_info->address1)
+      cod_info->table_select[1] =
+          new_choose_table(ix, cod_info->address1, cod_info->address2);
+
+    if (cod_info->big_values << 1 > cod_info->address2)
+      cod_info->table_select[2] =
+          new_choose_table(ix, cod_info->address2, cod_info->big_values << 1);
+  }
+}
+
+/*
+ * new_choose_table:
+ * -----------------
+ * Choose the Huffman table that will encode ix[begin..end] with
+ * the fewest bits.
+ * Note: This code contains knowledge about the sizes and characteristics
+ * of the Huffman tables as defined in the IS (Table B.7), and will not work
+ * with any arbitrary tables.
+ */
+int new_choose_table(int ix[GRANULE_SIZE], unsigned int begin,
+                     unsigned int end) {
+  int i, max;
+  int choice[2];
+  int sum[2];
+
+  max = ix_max(ix, begin, end);
+  if (!max)
+    return 0;
+
+  choice[0] = 0;
+  choice[1] = 0;
+
+  if (max < 15) {
+    /* try tables with no linbits */
+    for (i = 14; i--;)
+      if (shine_huffman_table[i].xlen > max) {
+        choice[0] = i;
+        break;
+      }
+
+    sum[0] = count_bit(ix, begin, end, choice[0]);
+
+    switch (choice[0]) {
+    case 2:
+      sum[1] = count_bit(ix, begin, end, 3);
+      if (sum[1] <= sum[0])
+        choice[0] = 3;
+      break;
+
+    case 5:
+      sum[1] = count_bit(ix, begin, end, 6);
+      if (sum[1] <= sum[0])
+        choice[0] = 6;
+      break;
+
+    case 7:
+      sum[1] = count_bit(ix, begin, end, 8);
+      if (sum[1] <= sum[0]) {
+        choice[0] = 8;
+        sum[0] = sum[1];
+      }
+      sum[1] = count_bit(ix, begin, end, 9);
+      if (sum[1] <= sum[0])
+        choice[0] = 9;
+      break;
+
+    case 10:
+      sum[1] = count_bit(ix, begin, end, 11);
+      if (sum[1] <= sum[0]) {
+        choice[0] = 11;
+        sum[0] = sum[1];
+      }
+      sum[1] = count_bit(ix, begin, end, 12);
+      if (sum[1] <= sum[0])
+        choice[0] = 12;
+      break;
+
+    case 13:
+      sum[1] = count_bit(ix, begin, end, 15);
+      if (sum[1] <= sum[0])
+        choice[0] = 15;
+      break;
+    }
+  } else {
+    /* try tables with linbits */
+    max -= 15;
+
+    for (i = 15; i < 24; i++)
+      if (shine_huffman_table[i].linmax >= max) {
+        choice[0] = i;
+        break;
+      }
+
+    for (i = 24; i < 32; i++)
+      if (shine_huffman_table[i].linmax >= max) {
+        choice[1] = i;
+        break;
+      }
+
+    sum[0] = count_bit(ix, begin, end, choice[0]);
+    sum[1] = count_bit(ix, begin, end, choice[1]);
+    if (sum[1] < sum[0])
+      choice[0] = choice[1];
+  }
+  return choice[0];
+}
+
+/*
+ * bigv_bitcount:
+ * --------------
+ * Function: Count the number of bits necessary to code the bigvalues region.
+ */
+int bigv_bitcount(int ix[GRANULE_SIZE], gr_info *gi) {
+  int bits = 0;
+  unsigned int table;
+
+  if ((table = gi->table_select[0])) /* region0 */
+    bits += count_bit(ix, 0, gi->address1, table);
+  if ((table = gi->table_select[1])) /* region1 */
+    bits += count_bit(ix, gi->address1, gi->address2, table);
+  if ((table = gi->table_select[2])) /* region2 */
+    bits += count_bit(ix, gi->address2, gi->address3, table);
+  return bits;
+}
+
+/*
+ * count_bit:
+ * ----------
+ * Function: Count the number of bits necessary to code the subregion.
+ */
+int count_bit(int ix[GRANULE_SIZE], unsigned int start, unsigned int end,
+              unsigned int table) {
+  unsigned linbits, ylen;
+  register int i, sum;
+  register int x, y;
+  const struct huffcodetab *h;
+
+  if (!table)
+    return 0;
+
+  h = &(shine_huffman_table[table]);
+  sum = 0;
+
+  ylen = h->ylen;
+  linbits = h->linbits;
+
+  if (table > 15) { /* ESC-table is used */
+    for (i = start; i < end; i += 2) {
+      x = ix[i];
+      y = ix[i + 1];
+      if (x > 14) {
+        x = 15;
+        sum += linbits;
+      }
+      if (y > 14) {
+        y = 15;
+        sum += linbits;
+      }
+
+      sum += h->hlen[(x * ylen) + y];
+      if (x)
+        sum++;
+      if (y)
+        sum++;
+    }
+  } else { /* No ESC-words */
+    for (i = start; i < end; i += 2) {
+      x = ix[i];
+      y = ix[i + 1];
+
+      sum += h->hlen[(x * ylen) + y];
+
+      if (x != 0)
+        sum++;
+      if (y != 0)
+        sum++;
+    }
+  }
+  return sum;
+}
+
+/*
+ * bin_search_StepSize:
+ * --------------------
+ * Succesive approximation approach to obtaining a initial quantizer
+ * step size.
+ * The following optional code written by Seymour Shlien
+ * will speed up the shine_outer_loop code which is called
+ * by iteration_loop. When BIN_SEARCH is defined, the
+ * shine_outer_loop function precedes the call to the function shine_inner_loop
+ * with a call to bin_search gain defined below, which
+ * returns a good starting quantizerStepSize.
+ */
+int bin_search_StepSize(int desired_rate, int ix[GRANULE_SIZE],
+                        gr_info *cod_info, shine_global_config *config) {
+  int bit, next, count;
+
+  next = -120;
+  count = 120;
+
+  do {
+    int half = count / 2;
+
+    if (quantize(ix, next + half, config) > 8192)
+      bit = 100000; /* fail */
+    else {
+      calc_runlen(ix, cod_info);           /* rzero,count1,big_values */
+      bit = count1_bitcount(ix, cod_info); /* count1_table selection */
+      subdivide(cod_info, config);         /* bigvalues sfb division */
+      bigv_tab_select(ix, cod_info);       /* codebook selection */
+      bit += bigv_bitcount(ix, cod_info);  /* bit count */
+    }
+
+    if (bit < desired_rate)
+      count = half;
+    else {
+      next += half;
+      count -= half;
+    }
+  } while (count > 1);
+
+  return next;
+}

+ 10 - 0
lib/shine/src/l3loop.h

@@ -0,0 +1,10 @@
+#ifndef L3LOOP_H
+#define L3LOOP_H
+
+#include "types.h"
+
+void shine_loop_initialise(shine_global_config *config);
+
+void shine_iteration_loop(shine_global_config *config);
+
+#endif

+ 149 - 0
lib/shine/src/l3mdct.c

@@ -0,0 +1,149 @@
+/* L3mdct */
+
+#include "l3mdct.h"
+#include "l3subband.h"
+#include "types.h"
+
+/* This is table B.9: coefficients for aliasing reduction */
+#define MDCT_CA(coef) (int32_t)(coef / sqrt(1.0 + (coef * coef)) * 0x7fffffff)
+#define MDCT_CS(coef) (int32_t)(1.0 / sqrt(1.0 + (coef * coef)) * 0x7fffffff)
+
+#define MDCT_CA0 MDCT_CA(-0.6)
+#define MDCT_CA1 MDCT_CA(-0.535)
+#define MDCT_CA2 MDCT_CA(-0.33)
+#define MDCT_CA3 MDCT_CA(-0.185)
+#define MDCT_CA4 MDCT_CA(-0.095)
+#define MDCT_CA5 MDCT_CA(-0.041)
+#define MDCT_CA6 MDCT_CA(-0.0142)
+#define MDCT_CA7 MDCT_CA(-0.0037)
+
+#define MDCT_CS0 MDCT_CS(-0.6)
+#define MDCT_CS1 MDCT_CS(-0.535)
+#define MDCT_CS2 MDCT_CS(-0.33)
+#define MDCT_CS3 MDCT_CS(-0.185)
+#define MDCT_CS4 MDCT_CS(-0.095)
+#define MDCT_CS5 MDCT_CS(-0.041)
+#define MDCT_CS6 MDCT_CS(-0.0142)
+#define MDCT_CS7 MDCT_CS(-0.0037)
+
+/*
+ * shine_mdct_initialise:
+ * -------------------
+ */
+void shine_mdct_initialise(shine_global_config *config) {
+  int m, k;
+
+  /* prepare the mdct coefficients */
+  for (m = 18; m--;)
+    for (k = 36; k--;)
+      /* combine window and mdct coefficients into a single table */
+      /* scale and convert to fixed point before storing */
+      config->mdct.cos_l[m][k] =
+          (int32_t)(sin(PI36 * (k + 0.5)) *
+                    cos((PI / 72) * (2 * k + 19) * (2 * m + 1)) * 0x7fffffff);
+}
+
+/*
+ * shine_mdct_sub:
+ * ------------
+ */
+void shine_mdct_sub(shine_global_config *config, int stride) {
+  /* note. we wish to access the array 'config->mdct_freq[2][2][576]' as
+   * [2][2][32][18]. (32*18=576),
+   */
+  int32_t(*mdct_enc)[18];
+
+  int ch, gr, band, j, k;
+  int32_t mdct_in[36];
+
+  for (ch = config->wave.channels; ch--;) {
+    for (gr = 0; gr < config->mpeg.granules_per_frame; gr++) {
+      /* set up pointer to the part of config->mdct_freq we're using */
+      mdct_enc = (int32_t(*)[18])config->mdct_freq[ch][gr];
+
+      /* polyphase filtering */
+      for (k = 0; k < 18; k += 2) {
+        shine_window_filter_subband(&config->buffer[ch],
+                                    &config->l3_sb_sample[ch][gr + 1][k][0], ch,
+                                    config, stride);
+        shine_window_filter_subband(&config->buffer[ch],
+                                    &config->l3_sb_sample[ch][gr + 1][k + 1][0],
+                                    ch, config, stride);
+        /* Compensate for inversion in the analysis filter
+         * (every odd index of band AND k)
+         */
+        for (band = 1; band < 32; band += 2)
+          config->l3_sb_sample[ch][gr + 1][k + 1][band] *= -1;
+      }
+
+      /* Perform imdct of 18 previous subband samples + 18 current subband
+       * samples */
+      for (band = 0; band < 32; band++) {
+        for (k = 18; k--;) {
+          mdct_in[k] = config->l3_sb_sample[ch][gr][k][band];
+          mdct_in[k + 18] = config->l3_sb_sample[ch][gr + 1][k][band];
+        }
+
+        /* Calculation of the MDCT
+         * In the case of long blocks ( block_type 0,1,3 ) there are
+         * 36 coefficients in the time domain and 18 in the frequency
+         * domain.
+         */
+        for (k = 18; k--;) {
+          int32_t vm;
+#ifdef __BORLANDC__
+          uint32_t vm_lo;
+#else
+          uint32_t vm_lo __attribute__((unused));
+#endif
+
+          mul0(vm, vm_lo, mdct_in[35], config->mdct.cos_l[k][35]);
+          for (j = 35; j; j -= 7) {
+            muladd(vm, vm_lo, mdct_in[j - 1], config->mdct.cos_l[k][j - 1]);
+            muladd(vm, vm_lo, mdct_in[j - 2], config->mdct.cos_l[k][j - 2]);
+            muladd(vm, vm_lo, mdct_in[j - 3], config->mdct.cos_l[k][j - 3]);
+            muladd(vm, vm_lo, mdct_in[j - 4], config->mdct.cos_l[k][j - 4]);
+            muladd(vm, vm_lo, mdct_in[j - 5], config->mdct.cos_l[k][j - 5]);
+            muladd(vm, vm_lo, mdct_in[j - 6], config->mdct.cos_l[k][j - 6]);
+            muladd(vm, vm_lo, mdct_in[j - 7], config->mdct.cos_l[k][j - 7]);
+          }
+          mulz(vm, vm_lo);
+          mdct_enc[band][k] = vm;
+        }
+
+        /* Perform aliasing reduction butterfly */
+        if (band != 0) {
+          cmuls(mdct_enc[band][0], mdct_enc[band - 1][17 - 0],
+                mdct_enc[band][0], mdct_enc[band - 1][17 - 0], MDCT_CS0,
+                MDCT_CA0);
+          cmuls(mdct_enc[band][1], mdct_enc[band - 1][17 - 1],
+                mdct_enc[band][1], mdct_enc[band - 1][17 - 1], MDCT_CS1,
+                MDCT_CA1);
+          cmuls(mdct_enc[band][2], mdct_enc[band - 1][17 - 2],
+                mdct_enc[band][2], mdct_enc[band - 1][17 - 2], MDCT_CS2,
+                MDCT_CA2);
+          cmuls(mdct_enc[band][3], mdct_enc[band - 1][17 - 3],
+                mdct_enc[band][3], mdct_enc[band - 1][17 - 3], MDCT_CS3,
+                MDCT_CA3);
+          cmuls(mdct_enc[band][4], mdct_enc[band - 1][17 - 4],
+                mdct_enc[band][4], mdct_enc[band - 1][17 - 4], MDCT_CS4,
+                MDCT_CA4);
+          cmuls(mdct_enc[band][5], mdct_enc[band - 1][17 - 5],
+                mdct_enc[band][5], mdct_enc[band - 1][17 - 5], MDCT_CS5,
+                MDCT_CA5);
+          cmuls(mdct_enc[band][6], mdct_enc[band - 1][17 - 6],
+                mdct_enc[band][6], mdct_enc[band - 1][17 - 6], MDCT_CS6,
+                MDCT_CA6);
+          cmuls(mdct_enc[band][7], mdct_enc[band - 1][17 - 7],
+                mdct_enc[band][7], mdct_enc[band - 1][17 - 7], MDCT_CS7,
+                MDCT_CA7);
+        }
+      }
+    }
+
+    /* Save latest granule's subband samples to be used in the next mdct call */
+    memcpy(config->l3_sb_sample[ch][0],
+           config->l3_sb_sample[ch][config->mpeg.granules_per_frame],
+           sizeof(config->l3_sb_sample[0][0]));
+  }
+}

+ 9 - 0
lib/shine/src/l3mdct.h

@@ -0,0 +1,9 @@
+#ifndef shine_MDCT_H
+#define shine_MDCT_H
+
+#include "types.h"
+
+void shine_mdct_initialise(shine_global_config *config);
+void shine_mdct_sub(shine_global_config *config, int stride);
+
+#endif

+ 128 - 0
lib/shine/src/l3subband.c

@@ -0,0 +1,128 @@
+/* L3SubBand */
+
+#include "l3subband.h"
+#include "tables.h"
+#include "types.h"
+
+/*
+ * shine_subband_initialise:
+ * ----------------------
+ * Calculates the analysis filterbank coefficients and rounds to the
+ * 9th decimal place accuracy of the filterbank tables in the ISO
+ * document.  The coefficients are stored in #filter#
+ */
+void shine_subband_initialise(shine_global_config *config) {
+  int i, j;
+  double filter;
+
+  for (i = MAX_CHANNELS; i--;) {
+    config->subband.off[i] = 0;
+    memset(config->subband.x[i], 0, sizeof(config->subband.x[i]));
+  }
+
+  for (i = SBLIMIT; i--;)
+    for (j = 64; j--;) {
+      if ((filter = 1e9 * cos((double)((2 * i + 1) * (16 - j) * PI64))) >= 0)
+        modf(filter + 0.5, &filter);
+      else
+        modf(filter - 0.5, &filter);
+      /* scale and convert to fixed point before storing */
+      config->subband.fl[i][j] = (int32_t)(filter * (0x7fffffff * 1e-9));
+    }
+}
+
+/*
+ * shine_window_filter_subband:
+ * -------------------------
+ * Overlapping window on PCM samples
+ * 32 16-bit pcm samples are scaled to fractional 2's complement and
+ * concatenated to the end of the window buffer #x#. The updated window
+ * buffer #x# is then windowed by the analysis window #shine_enwindow# to
+ * produce the windowed sample #z# Calculates the analysis filter bank
+ * coefficients The windowed samples #z# is filtered by the digital filter
+ * matrix #filter# to produce the subband samples #s#. This done by first
+ * selectively picking out values from the windowed samples, and then
+ * multiplying them by the filter matrix, producing 32 subband samples.
+ */
+void shine_window_filter_subband(int16_t **buffer, int32_t s[SBLIMIT], int ch,
+                                 shine_global_config *config, int stride) {
+  int32_t y[64];
+  int i, j;
+  int16_t *ptr = *buffer;
+
+  /* replace 32 oldest samples with 32 new samples */
+  for (i = 32; i--;) {
+    config->subband.x[ch][i + config->subband.off[ch]] = ((int32_t)*ptr) << 16;
+    ptr += stride;
+  }
+  *buffer = ptr;
+
+  for (i = 64; i--;) {
+    int32_t s_value;
+#ifdef __BORLANDC__
+    uint32_t s_value_lo;
+#else
+    uint32_t s_value_lo __attribute__((unused));
+#endif
+
+    mul0(s_value, s_value_lo,
+         config->subband
+             .x[ch][(config->subband.off[ch] + i + (0 << 6)) & (HAN_SIZE - 1)],
+         shine_enwindow[i + (0 << 6)]);
+    muladd(s_value, s_value_lo,
+           config->subband.x[ch][(config->subband.off[ch] + i + (1 << 6)) &
+                                 (HAN_SIZE - 1)],
+           shine_enwindow[i + (1 << 6)]);
+    muladd(s_value, s_value_lo,
+           config->subband.x[ch][(config->subband.off[ch] + i + (2 << 6)) &
+                                 (HAN_SIZE - 1)],
+           shine_enwindow[i + (2 << 6)]);
+    muladd(s_value, s_value_lo,
+           config->subband.x[ch][(config->subband.off[ch] + i + (3 << 6)) &
+                                 (HAN_SIZE - 1)],
+           shine_enwindow[i + (3 << 6)]);
+    muladd(s_value, s_value_lo,
+           config->subband.x[ch][(config->subband.off[ch] + i + (4 << 6)) &
+                                 (HAN_SIZE - 1)],
+           shine_enwindow[i + (4 << 6)]);
+    muladd(s_value, s_value_lo,
+           config->subband.x[ch][(config->subband.off[ch] + i + (5 << 6)) &
+                                 (HAN_SIZE - 1)],
+           shine_enwindow[i + (5 << 6)]);
+    muladd(s_value, s_value_lo,
+           config->subband.x[ch][(config->subband.off[ch] + i + (6 << 6)) &
+                                 (HAN_SIZE - 1)],
+           shine_enwindow[i + (6 << 6)]);
+    muladd(s_value, s_value_lo,
+           config->subband.x[ch][(config->subband.off[ch] + i + (7 << 6)) &
+                                 (HAN_SIZE - 1)],
+           shine_enwindow[i + (7 << 6)]);
+    mulz(s_value, s_value_lo);
+    y[i] = s_value;
+  }
+
+  config->subband.off[ch] = (config->subband.off[ch] + 480) &
+                            (HAN_SIZE - 1); /* offset is modulo (HAN_SIZE)*/
+
+  for (i = SBLIMIT; i--;) {
+    int32_t s_value;
+#ifdef __BORLANDC__
+    uint32_t s_value_lo;
+#else
+    uint32_t s_value_lo __attribute__((unused));
+#endif
+
+    mul0(s_value, s_value_lo, config->subband.fl[i][63], y[63]);
+    for (j = 63; j; j -= 7) {
+      muladd(s_value, s_value_lo, config->subband.fl[i][j - 1], y[j - 1]);
+      muladd(s_value, s_value_lo, config->subband.fl[i][j - 2], y[j - 2]);
+      muladd(s_value, s_value_lo, config->subband.fl[i][j - 3], y[j - 3]);
+      muladd(s_value, s_value_lo, config->subband.fl[i][j - 4], y[j - 4]);
+      muladd(s_value, s_value_lo, config->subband.fl[i][j - 5], y[j - 5]);
+      muladd(s_value, s_value_lo, config->subband.fl[i][j - 6], y[j - 6]);
+      muladd(s_value, s_value_lo, config->subband.fl[i][j - 7], y[j - 7]);
+    }
+    mulz(s_value, s_value_lo);
+    s[i] = s_value;
+  }
+}

+ 11 - 0
lib/shine/src/l3subband.h

@@ -0,0 +1,11 @@
+#ifndef L3SUBBAND_H
+#define L3SUBBAND_H
+
+#include "types.h"
+#include <stdint.h>
+
+void shine_subband_initialise(shine_global_config *config);
+void shine_window_filter_subband(int16_t **buffer, int32_t s[SBLIMIT], int k,
+                                 shine_global_config *config, int stride);
+
+#endif

+ 208 - 0
lib/shine/src/layer3.c

@@ -0,0 +1,208 @@
+/* layer3.c */
+
+#include "layer3.h"
+#include "bitstream.h"
+#include "l3bitstream.h"
+#include "l3loop.h"
+#include "l3mdct.h"
+#include "l3subband.h"
+#include "tables.h"
+#include "types.h"
+
+static int granules_per_frame[4] = {
+    1,  /* MPEG 2.5 */
+    -1, /* Reserved */
+    1,  /* MPEG II */
+    2   /* MPEG I */
+};
+
+/* Set default values for important vars */
+void shine_set_config_mpeg_defaults(shine_mpeg_t *mpeg) {
+  mpeg->bitr = 128;
+  mpeg->emph = NONE;
+  mpeg->copyright = 0;
+  mpeg->original = 1;
+}
+
+int shine_mpeg_version(int samplerate_index) {
+  /* Pick mpeg version according to samplerate index. */
+  if (samplerate_index < 3)
+    /* First 3 samplerates are for MPEG-I */
+    return MPEG_I;
+  else if (samplerate_index < 6)
+    /* Then it's MPEG-II */
+    return MPEG_II;
+  else
+    /* Finally, MPEG-2.5 */
+    return MPEG_25;
+}
+
+int shine_find_samplerate_index(int freq) {
+  int i;
+
+  for (i = 0; i < 9; i++)
+    if (freq == samplerates[i])
+      return i;
+
+  return -1; /* error - not a valid samplerate for encoder */
+}
+
+int shine_find_bitrate_index(int bitr, int mpeg_version) {
+  int i;
+
+  for (i = 0; i < 16; i++)
+    if (bitr == bitrates[i][mpeg_version])
+      return i;
+
+  return -1; /* error - not a valid samplerate for encoder */
+}
+
+int shine_check_config(int freq, int bitr) {
+  int samplerate_index, bitrate_index, mpeg_version;
+
+  samplerate_index = shine_find_samplerate_index(freq);
+  if (samplerate_index < 0)
+    return -1;
+
+  mpeg_version = shine_mpeg_version(samplerate_index);
+
+  bitrate_index = shine_find_bitrate_index(bitr, mpeg_version);
+  if (bitrate_index < 0)
+    return -1;
+
+  return mpeg_version;
+}
+
+int shine_samples_per_pass(shine_t s) {
+  return s->mpeg.granules_per_frame * GRANULE_SIZE;
+}
+
+/* Compute default encoding values. */
+shine_global_config *shine_initialise(shine_config_t *pub_config) {
+  double avg_slots_per_frame;
+  shine_global_config *config;
+
+  if (shine_check_config(pub_config->wave.samplerate, pub_config->mpeg.bitr) <
+      0)
+    return NULL;
+
+  config = calloc(1, sizeof(shine_global_config));
+  if (config == NULL)
+    return config;
+
+  shine_subband_initialise(config);
+  shine_mdct_initialise(config);
+  shine_loop_initialise(config);
+
+  /* Copy public config. */
+  config->wave.channels = pub_config->wave.channels;
+  config->wave.samplerate = pub_config->wave.samplerate;
+  config->mpeg.mode = pub_config->mpeg.mode;
+  config->mpeg.bitr = pub_config->mpeg.bitr;
+  config->mpeg.emph = pub_config->mpeg.emph;
+  config->mpeg.copyright = pub_config->mpeg.copyright;
+  config->mpeg.original = pub_config->mpeg.original;
+
+  /* Set default values. */
+  config->ResvMax = 0;
+  config->ResvSize = 0;
+  config->mpeg.layer = LAYER_III;
+  config->mpeg.crc = 0;
+  config->mpeg.ext = 0;
+  config->mpeg.mode_ext = 0;
+  config->mpeg.bits_per_slot = 8;
+
+  config->mpeg.samplerate_index =
+      shine_find_samplerate_index(config->wave.samplerate);
+  config->mpeg.version = shine_mpeg_version(config->mpeg.samplerate_index);
+  config->mpeg.bitrate_index =
+      shine_find_bitrate_index(config->mpeg.bitr, config->mpeg.version);
+  config->mpeg.granules_per_frame = granules_per_frame[config->mpeg.version];
+
+  /* Figure average number of 'slots' per frame. */
+  avg_slots_per_frame =
+      ((double)config->mpeg.granules_per_frame * GRANULE_SIZE /
+       ((double)config->wave.samplerate)) *
+      (1000 * (double)config->mpeg.bitr / (double)config->mpeg.bits_per_slot);
+
+  config->mpeg.whole_slots_per_frame = (int)avg_slots_per_frame;
+
+  config->mpeg.frac_slots_per_frame =
+      avg_slots_per_frame - (double)config->mpeg.whole_slots_per_frame;
+  config->mpeg.slot_lag = -config->mpeg.frac_slots_per_frame;
+
+  if (config->mpeg.frac_slots_per_frame == 0)
+    config->mpeg.padding = 0;
+
+  shine_open_bit_stream(&config->bs, BUFFER_SIZE);
+
+  memset((char *)&config->side_info, 0, sizeof(shine_side_info_t));
+
+  /* determine the mean bitrate for main data */
+  if (config->mpeg.granules_per_frame == 2) /* MPEG 1 */
+    config->sideinfo_len = 8 * ((config->wave.channels == 1) ? 4 + 17 : 4 + 32);
+  else /* MPEG 2 */
+    config->sideinfo_len = 8 * ((config->wave.channels == 1) ? 4 + 9 : 4 + 17);
+
+  return config;
+}
+
+static unsigned char *shine_encode_buffer_internal(shine_global_config *config,
+                                                   int *written, int stride) {
+  if (config->mpeg.frac_slots_per_frame) {
+    config->mpeg.padding =
+        (config->mpeg.slot_lag <= (config->mpeg.frac_slots_per_frame - 1.0));
+    config->mpeg.slot_lag +=
+        (config->mpeg.padding - config->mpeg.frac_slots_per_frame);
+  }
+
+  config->mpeg.bits_per_frame =
+      8 * (config->mpeg.whole_slots_per_frame + config->mpeg.padding);
+  config->mean_bits = (config->mpeg.bits_per_frame - config->sideinfo_len) /
+                      config->mpeg.granules_per_frame;
+
+  /* apply mdct to the polyphase output */
+  shine_mdct_sub(config, stride);
+
+  /* bit and noise allocation */
+  shine_iteration_loop(config);
+
+  /* write the frame to the bitstream */
+  shine_format_bitstream(config);
+
+  /* Return data. */
+  *written = config->bs.data_position;
+  config->bs.data_position = 0;
+
+  return config->bs.data;
+}
+
+unsigned char *shine_encode_buffer(shine_global_config *config, int16_t **data,
+                                   int *written) {
+  config->buffer[0] = data[0];
+  if (config->wave.channels == 2)
+    config->buffer[1] = data[1];
+
+  return shine_encode_buffer_internal(config, written, 1);
+}
+
+unsigned char *shine_encode_buffer_interleaved(shine_global_config *config,
+                                               int16_t *data, int *written) {
+  config->buffer[0] = data;
+  if (config->wave.channels == 2)
+    config->buffer[1] = data + 1;
+
+  return shine_encode_buffer_internal(config, written, config->wave.channels);
+}
+
+unsigned char *shine_flush(shine_global_config *config, int *written) {
+  *written = config->bs.data_position;
+  config->bs.data_position = 0;
+
+  return config->bs.data;
+}
+
+void shine_close(shine_global_config *config) {
+  shine_close_bit_stream(&config->bs);
+  free(config);
+}

+ 146 - 0
lib/shine/src/layer3.h

@@ -0,0 +1,146 @@
+#ifndef LAYER3_H
+#define LAYER3_H
+
+#include <stdint.h>
+
+/* This is the struct used to tell the encoder about the input PCM */
+
+enum channels { PCM_MONO = 1, PCM_STEREO = 2 };
+
+enum mpeg_versions { MPEG_I = 3, MPEG_II = 2, MPEG_25 = 0 };
+
+/* Only Layer III currently implemented. */
+enum mpeg_layers { LAYER_III = 1 };
+
+typedef struct {
+  enum channels channels;
+  int samplerate;
+} shine_wave_t;
+
+/* This is the struct the encoder uses to tell the encoder about the output MP3
+ */
+
+enum modes { STEREO = 0, JOINT_STEREO = 1, DUAL_CHANNEL = 2, MONO = 3 };
+
+enum emph { NONE = 0, MU50_15 = 1, CITT = 3 };
+
+typedef struct {
+  enum modes mode; /* Stereo mode */
+  int bitr;        /* Must conform to known bitrate */
+  enum emph emph;  /* De-emphasis */
+  int copyright;
+  int original;
+} shine_mpeg_t;
+
+typedef struct {
+  shine_wave_t wave;
+  shine_mpeg_t mpeg;
+} shine_config_t;
+
+/* Tables of supported audio parameters & format.
+ *
+ * Valid samplerates and bitrates.
+ * const int samplerates[9] = {
+ *   44100, 48000, 32000, // MPEG-I
+ *   22050, 24000, 16000, // MPEG-II
+ *   11025, 12000, 8000   // MPEG-2.5
+ * };
+ *
+ * const int bitrates[16][4] = {
+ * //  MPEG version:
+ * //  2.5, reserved,  II,  I
+ *   { -1,  -1,        -1,  -1},
+ *   { 8,   -1,         8,  32},
+ *   { 16,  -1,        16,  40},
+ *   { 24,  -1,        24,  48},
+ *   { 32,  -1,        32,  56},
+ *   { 40,  -1,        40,  64},
+ *   { 48,  -1,        48,  80},
+ *   { 56,  -1,        56,  96},
+ *   { 64,  -1,        64, 112},
+ *   { 80,  -1,        80, 128},
+ *   { 96,  -1,        96, 160},
+ *   {112,  -1,       112, 192},
+ *   {128,  -1,       128, 224},
+ *   {144,  -1,       144, 256},
+ *   {160,  -1,       160, 320},
+ *   { -1,  -1,        -1,  -1}
+ *  };
+ *
+ */
+
+/* Abtract type for the shine encoder handle. */
+typedef struct shine_global_flags *shine_t;
+
+/* Fill in a `mpeg_t` structure with default values. */
+void shine_set_config_mpeg_defaults(shine_mpeg_t *mpeg);
+
+/* Check if a given bitrate is supported by the encoder (see `bitrates` above
+ * for a list of acceptable values. */
+int shine_find_bitrate_index(int bitr, int mpeg_version);
+
+/* Check if a given samplerate is supported by the encoder (see `samplerates`
+ * above for a list of acceptable values. */
+int shine_find_samplerate_index(int freq);
+
+/* Returns the MPEG version used for the given samplerate index. See above
+ * `mpeg_versions` for a list of possible values. */
+int shine_mpeg_version(int samplerate_index);
+
+/* Check if a given bitrate and samplerate is supported by the encoder (see
+ * `samplerates` and `bitrates` above for a list of acceptable values).
+ *
+ * Returns -1 on error, mpeg_version on success. */
+int shine_check_config(int freq, int bitr);
+
+/* Pass a pointer to a `config_t` structure and returns an initialized
+ * encoder.
+ *
+ * Configuration data is copied over to the encoder. It is not possible
+ * to change its values after initializing the encoder at the moment.
+ *
+ * Checking for valid configuration values is left for the application to
+ * implement. You can use the `shine_find_bitrate_index` and
+ * `shine_find_samplerate_index` functions or the `bitrates` and
+ * `samplerates` arrays above to check those parameters. Mone and stereo
+ * mode for wave and mpeg should also be consistent with each other.
+ *
+ * This function returns NULL if it was not able to allocate memory data for
+ * the encoder. */
+shine_t shine_initialise(shine_config_t *config);
+
+/* Maximun possible value for the function below. */
+#define SHINE_MAX_SAMPLES 1152
+
+/* Returns audio samples expected in each frame. */
+int shine_samples_per_pass(shine_t s);
+
+/* Encode audio data. Source data must have `shine_samples_per_pass(s)` audio
+ * samples per channels. Mono encoder only expect one channel.
+ *
+ * Returns a pointer to freshly encoded data while `written` contains the size
+ * of available data. This pointer's memory is handled by the library and is
+ * only valid until the next call to `shine_encode_buffer` or `shine_close` and
+ * may be NULL if no data was written. */
+unsigned char *shine_encode_buffer(shine_t s, int16_t **data, int *written);
+
+/* Encode interleaved audio data. Source data must have
+ * `shine_samples_per_pass(s)` audio samples per channels. Mono encoder only
+ * expect one channel.
+ *
+ * Returns a pointer to freshly encoded data while `written` contains the size
+ * of available data. This pointer's memory is handled by the library and is
+ * only valid until the next call to `shine_encode_buffer` or `shine_close` and
+ * may be NULL if no data was written. */
+unsigned char *shine_encode_buffer_interleaved(shine_t s, int16_t *data,
+                                               int *written);
+
+/* Flush all data currently in the encoding buffer. Should be used before
+ * closing the encoder, to make all encoded data has been written. */
+unsigned char *shine_flush(shine_t s, int *written);
+
+/* Close an encoder, freeing all associated memory. Encoder handler is not
+ * valid after this call. */
+void shine_close(shine_t s);
+
+#endif

+ 50 - 0
lib/shine/src/mult_mips_gcc.h

@@ -0,0 +1,50 @@
+#include <stdint.h>
+
+#define mul(a, b)                                                              \
+  ({                                                                           \
+    register int32_t res;                                                      \
+    __asm__ __volatile__("mult %0, %1" : : "r"(a), "r"(b));                    \
+    __asm__ __volatile__("mfhi %0" : "=r"(res));                               \
+    res;                                                                       \
+  })
+
+#define mul0(hi, lo, a, b)                                                     \
+  __asm__ __volatile__("mult %0, %1" : : "r"(a), "r"(b))
+
+#define muladd(hi, lo, a, b)                                                   \
+  __asm__ __volatile__("madd %0, %1" : : "r"(a), "r"(b))
+
+#define mulsub(hi, lo, a, b)                                                   \
+  __asm__ __volatile__("msub %0, %1" : : "r"(a), "r"(b))
+
+#define mulz(hi, lo)                                                           \
+  do {                                                                         \
+    register int32_t t;                                                        \
+    __asm__ __volatile__("mfhi %0" : "=r"(t));                                 \
+    (hi) = t;                                                                  \
+  } while (0)
+
+#define cmuls(dre, dim, are, aim, bre, bim)                                    \
+  do {                                                                         \
+    register int32_t t1, t2, tre;                                              \
+    __asm__ __volatile__("mult %0, %1" : : "r"(are), "r"(bre));                \
+    __asm__ __volatile__("msub %0, %1" : : "r"(aim), "r"(bim));                \
+    __asm__ __volatile__("mfhi %0; mflo %1" : "=r"(t1), "=r"(t2));             \
+    tre = (t1 << 1) | ((uint32_t)t2 >> 31);                                    \
+    __asm__ __volatile__("mult %0, %1" : : "r"(are), "r"(bim));                \
+    __asm__ __volatile__("madd %0, %1" : : "r"(bre), "r"(aim));                \
+    dre = tre;                                                                 \
+    __asm__ __volatile__("mfhi %0; mflo %1" : "=r"(t1), "=r"(t2));             \
+    dim = (t1 << 1) | ((uint32_t)t2 >> 31);                                    \
+  } while (0)
+
+#if __mips_isa_rev >= 2
+static inline uint32_t SWAB32(uint32_t x) {
+  __asm__("	wsbh	%0, %1			\n"
+          "	rotr	%0, %0, 16		\n"
+          : "=r"(x)
+          : "r"(x));
+  return x;
+}
+#define SWAB32 SWAB32
+#endif

+ 40 - 0
lib/shine/src/mult_noarch_gcc.h

@@ -0,0 +1,40 @@
+#include <stdint.h>
+
+#ifndef mul
+#define mul(a, b) (int32_t)((((int64_t)a) * ((int64_t)b)) >> 32)
+#endif
+
+#ifndef muls
+#define muls(a, b) (int32_t)((((int64_t)a) * ((int64_t)b)) >> 31)
+#endif
+
+#ifndef mulr
+#define mulr(a, b)                                                             \
+  (int32_t)(((((int64_t)a) * ((int64_t)b)) + 0x80000000LL) >> 32)
+#endif
+
+#ifndef mulsr
+#define mulsr(a, b)                                                            \
+  (int32_t)(((((int64_t)a) * ((int64_t)b)) + 0x40000000LL) >> 31)
+#endif
+
+#ifndef mul0
+#define mul0(hi, lo, a, b) ((hi) = mul((a), (b)))
+#define muladd(hi, lo, a, b) ((hi) += mul((a), (b)))
+#define mulsub(hi, lo, a, b) ((hi) -= mul((a), (b)))
+#define mulz(hi, lo)
+#endif
+
+#ifndef cmuls
+#define cmuls(dre, dim, are, aim, bre, bim)                                    \
+  do {                                                                         \
+    int32_t tre;                                                               \
+    (tre) = (int32_t)(((int64_t)(are) * (int64_t)(bre) -                       \
+                       (int64_t)(aim) * (int64_t)(bim)) >>                     \
+                      31);                                                     \
+    (dim) = (int32_t)(((int64_t)(are) * (int64_t)(bim) +                       \
+                       (int64_t)(aim) * (int64_t)(bre)) >>                     \
+                      31);                                                     \
+    (dre) = tre;                                                               \
+  } while (0)
+#endif

+ 103 - 0
lib/shine/src/mult_sarm_gcc.h

@@ -0,0 +1,103 @@
+#include <stdint.h>
+
+/* Fractional multiply */
+#if __ARM_ARCH >= 6
+#define mul(x, y)                                                              \
+  ({                                                                           \
+    register int32_t result;                                                   \
+    asm("smmul %0, %2, %1" : "=r"(result) : "r"(x), "r"(y));                   \
+    result;                                                                    \
+  })
+#else
+#define mul(x, y)                                                              \
+  ({                                                                           \
+    register int32_t result;                                                   \
+    asm("smull r3, %0, %2, %1" : "=r"(result) : "r"(x), "r"(y) : "r3");        \
+    result;                                                                    \
+  })
+#endif
+
+/* Fractional multiply with single bit left shift. */
+#define muls(x, y)                                                             \
+  ({                                                                           \
+    register int32_t result;                                                   \
+    asm("smull r3, %0, %2, %1\n\t"                                             \
+        "movs r3, r3, lsl #1\n\t"                                              \
+        "adc %0, %0, %0"                                                       \
+        : "=r"(result)                                                         \
+        : "r"(x), "r"(y)                                                       \
+        : "r3", "cc");                                                         \
+    result;                                                                    \
+  })
+
+#if __ARM_ARCH >= 6
+#define mulr(x, y)                                                             \
+  ({                                                                           \
+    register int32_t result;                                                   \
+    asm("smmulr %0, %2, %1" : "=r"(result) : "r"(x), "r"(y));                  \
+    result;                                                                    \
+  })
+#else
+#define mulr(x, y)                                                             \
+  ({                                                                           \
+    register int32_t result;                                                   \
+    asm("smull r3, %0, %2, %1\n\t"                                             \
+        "adds r3, r3, #0x80000000\n\t"                                         \
+        "adc %0, %0, #0"                                                       \
+        : "=r"(result)                                                         \
+        : "r"(x), "r"(y)                                                       \
+        : "r3", "cc");                                                         \
+    result;                                                                    \
+  })
+#endif
+
+#define mulsr(x, y)                                                            \
+  ({                                                                           \
+    register int32_t result;                                                   \
+    asm("smull r3, %0, %1, %2\n\t"                                             \
+        "movs r3, r3, lsl #1\n\t"                                              \
+        "adc %0, %0, %0\n\t"                                                   \
+        "adds r3, r3, #0x80000000\n\t"                                         \
+        "adc %0, %0, #0"                                                       \
+        : "=r"(result)                                                         \
+        : "r"(x), "r"(y)                                                       \
+        : "r3", "cc");                                                         \
+    result;                                                                    \
+  })
+
+#define mul0(hi, lo, a, b)                                                     \
+  asm("smull %0, %1, %2, %3" : "=r"(lo), "=r"(hi) : "r"(a), "r"(b))
+
+#define muladd(hi, lo, a, b)                                                   \
+  asm("smlal %0, %1, %2, %3" : "+r"(lo), "+r"(hi) : "r"(a), "r"(b))
+
+#define mulsub(hi, lo, a, b)                                                   \
+  asm("smlal %0, %1, %2, %3" : "+r"(lo), "+r"(hi) : "r"(a), "r"(-(b)))
+
+#define mulz(hi, lo)
+
+#define cmuls(dre, dim, are, aim, bre, bim)                                    \
+  do {                                                                         \
+    register int32_t tre, tim;                                                 \
+    asm("smull r3, %0, %2, %4\n\t"                                             \
+        "smlal r3, %0, %3, %5\n\t"                                             \
+        "movs r3, r3, lsl #1\n\t"                                              \
+        "adc %0, %0, %0\n\t"                                                   \
+        "smull r3, %1, %2, %6\n\t"                                             \
+        "smlal r3, %1, %4, %3\n\t"                                             \
+        "movs r3, r3, lsl #1\n\t"                                              \
+        "adc %1, %1, %1\n\t"                                                   \
+        : "=&r"(tre), "=&r"(tim)                                               \
+        : "r"(are), "r"(aim), "r"(bre), "r"(-(bim)), "r"(bim)                  \
+        : "r3", "cc");                                                         \
+    dre = tre;                                                                 \
+    dim = tim;                                                                 \
+  } while (0)
+
+#if __ARM_ARCH >= 6
+static inline uint32_t SWAB32(uint32_t x) {
+  asm("rev %0, %1" : "=r"(x) : "r"(x));
+  return x;
+}
+#define SWAB32 SWAB32
+#endif

+ 128 - 0
lib/shine/src/reservoir.c

@@ -0,0 +1,128 @@
+/* reservoir.c
+ * Layer3 bit reservoir: Described in C.1.5.4.2.2 of the IS
+ */
+
+#include "reservoir.h"
+#include "bitstream.h"
+#include "huffman.h"
+#include "l3bitstream.h"
+#include "l3loop.h"
+#include "layer3.h"
+#include "types.h"
+
+/*
+ * shine_max_reservoir_bits:
+ * ------------
+ * Called at the beginning of each granule to get the max bit
+ * allowance for the current granule based on reservoir size
+ * and perceptual entropy.
+ */
+int shine_max_reservoir_bits(double *pe, shine_global_config *config) {
+  int more_bits, max_bits, add_bits, over_bits;
+  int mean_bits = config->mean_bits;
+
+  mean_bits /= config->wave.channels;
+  max_bits = mean_bits;
+
+  if (max_bits > 4095)
+    max_bits = 4095;
+  if (!config->ResvMax)
+    return max_bits;
+
+  more_bits = *pe * 3.1 - mean_bits;
+  add_bits = 0;
+  if (more_bits > 100) {
+    int frac = (config->ResvSize * 6) / 10;
+
+    if (frac < more_bits)
+      add_bits = frac;
+    else
+      add_bits = more_bits;
+  }
+  over_bits = config->ResvSize - ((config->ResvMax << 3) / 10) - add_bits;
+  if (over_bits > 0)
+    add_bits += over_bits;
+
+  max_bits += add_bits;
+  if (max_bits > 4095)
+    max_bits = 4095;
+  return max_bits;
+}
+
+/*
+ * shine_ResvAdjust:
+ * -----------
+ * Called after a granule's bit allocation. Readjusts the size of
+ * the reservoir to reflect the granule's usage.
+ */
+void shine_ResvAdjust(gr_info *gi, shine_global_config *config) {
+  config->ResvSize +=
+      (config->mean_bits / config->wave.channels) - gi->part2_3_length;
+}
+
+/*
+ * shine_ResvFrameEnd:
+ * -------------
+ * Called after all granules in a frame have been allocated. Makes sure
+ * that the reservoir size is within limits, possibly by adding stuffing
+ * bits. Note that stuffing bits are added by increasing a granule's
+ * part2_3_length. The bitstream formatter will detect this and write the
+ * appropriate stuffing bits to the bitstream.
+ */
+void shine_ResvFrameEnd(shine_global_config *config) {
+  gr_info *gi;
+  int gr, ch, ancillary_pad, stuffingBits;
+  int over_bits;
+  shine_side_info_t *l3_side = &config->side_info;
+
+  ancillary_pad = 0;
+
+  /* just in case mean_bits is odd, this is necessary... */
+  if ((config->wave.channels == 2) && (config->mean_bits & 1))
+    config->ResvSize += 1;
+
+  over_bits = config->ResvSize - config->ResvMax;
+  if (over_bits < 0)
+    over_bits = 0;
+
+  config->ResvSize -= over_bits;
+  stuffingBits = over_bits + ancillary_pad;
+
+  /* we must be byte aligned */
+  if ((over_bits = config->ResvSize % 8)) {
+    stuffingBits += over_bits;
+    config->ResvSize -= over_bits;
+  }
+
+  if (stuffingBits) {
+    /*
+     * plan a: put all into the first granule
+     * This was preferred by someone designing a
+     * real-time decoder...
+     */
+    gi = (gr_info *)&(l3_side->gr[0].ch[0]);
+
+    if (gi->part2_3_length + stuffingBits < 4095)
+      gi->part2_3_length += stuffingBits;
+    else {
+      /* plan b: distribute throughout the granules */
+      for (gr = 0; gr < config->mpeg.granules_per_frame; gr++)
+        for (ch = 0; ch < config->wave.channels; ch++) {
+          int extraBits, bitsThisGr;
+          gr_info *gi = (gr_info *)&(l3_side->gr[gr].ch[ch]);
+          if (!stuffingBits)
+            break;
+          extraBits = 4095 - gi->part2_3_length;
+          bitsThisGr = extraBits < stuffingBits ? extraBits : stuffingBits;
+          gi->part2_3_length += bitsThisGr;
+          stuffingBits -= bitsThisGr;
+        }
+      /*
+       * If any stuffing bits remain, we elect to spill them
+       * into ancillary data. The bitstream formatter will do this if
+       * l3side->resvDrain is set
+       */
+      l3_side->resvDrain = stuffingBits;
+    }
+  }
+}

+ 11 - 0
lib/shine/src/reservoir.h

@@ -0,0 +1,11 @@
+#ifndef RESERVOIR_H
+#define RESERVOIR_H
+
+#include "types.h"
+
+void shine_ResvFrameBegin(int frameLength, shine_global_config *config);
+int shine_max_reservoir_bits(double *pe, shine_global_config *config);
+void shine_ResvAdjust(gr_info *gi, shine_global_config *config);
+void shine_ResvFrameEnd(shine_global_config *config);
+
+#endif

+ 177 - 0
lib/shine/src/tables.c

@@ -0,0 +1,177 @@
+/* tables.c
+ *
+ * Here are MPEG1 Table B.8 and MPEG2 Table B.1 -- Layer III scalefactor bands.
+ * Index into this using a method such as:
+ *    idx  = fr_ps->header->sampling_frequency + (fr_ps->header->version * 3)
+ */
+
+#include "tables.h"
+
+const int shine_slen1_tab[16] = {0, 0, 0, 0, 3, 1, 1, 1,
+                                 2, 2, 2, 3, 3, 3, 4, 4};
+const int shine_slen2_tab[16] = {0, 1, 2, 3, 0, 1, 2, 3,
+                                 1, 2, 3, 1, 2, 3, 2, 3};
+
+/* Valid samplerates and bitrates. */
+const int samplerates[9] = {
+    44100, 48000, 32000, /* MPEG-I */
+    22050, 24000, 16000, /* MPEG-II */
+    11025, 12000, 8000   /* MPEG-2.5 */
+};
+
+const int bitrates[16][4] = {
+    /* MPEG version:
+     * 2.5, reserved, II, I */
+    {-1, -1, -1, -1},   {8, -1, 8, 32},     {16, -1, 16, 40},
+    {24, -1, 24, 48},   {32, -1, 32, 56},   {40, -1, 40, 64},
+    {48, -1, 48, 80},   {56, -1, 56, 96},   {64, -1, 64, 112},
+    {-1, -1, 80, 128},  {-1, -1, 96, 160},  {-1, -1, 112, 192},
+    {-1, -1, 128, 224}, {-1, -1, 144, 256}, {-1, -1, 160, 320},
+    {-1, -1, -1, -1}};
+
+const int shine_scale_fact_band_index[9][23] = {
+    /* MPEG-I */
+    /* Table B.8.b: 44.1 kHz */
+    {0,  4,  8,   12,  16,  20,  24,  30,  36,  44,  52, 62,
+     74, 90, 110, 134, 162, 196, 238, 288, 342, 418, 576},
+    /* Table B.8.c: 48 kHz */
+    {0,  4,  8,   12,  16,  20,  24,  30,  36,  42,  50, 60,
+     72, 88, 106, 128, 156, 190, 230, 276, 330, 384, 576},
+    /* Table B.8.a: 32 kHz */
+    {0,  4,   8,   12,  16,  20,  24,  30,  36,  44,  54, 66,
+     82, 102, 126, 156, 194, 240, 296, 364, 448, 550, 576},
+    /* MPEG-II */
+    /* Table B.2.b: 22.05 kHz */
+    {0,   6,   12,  18,  24,  30,  36,  44,  54,  66,  80, 96,
+     116, 140, 168, 200, 238, 284, 336, 396, 464, 522, 576},
+    /* Table B.2.c: 24 kHz */
+    {0,   6,   12,  18,  24,  30,  36,  44,  54,  66,  80, 96,
+     114, 136, 162, 194, 232, 278, 330, 394, 464, 540, 576},
+    /* Table B.2.a: 16 kHz */
+    {0,   6,   12,  18,  24,  30,  36,  44,  45,  66,  80, 96,
+     116, 140, 168, 200, 238, 248, 336, 396, 464, 522, 576},
+
+    /* MPEG-2.5 */
+    /* 11.025 kHz */
+    {0,   6,   12,  18,  24,  30,  36,  44,  54,  66,  80, 96,
+     116, 140, 168, 200, 238, 284, 336, 396, 464, 522, 576},
+    /* 12 kHz */
+    {0,   6,   12,  18,  24,  30,  36,  44,  54,  66,  80, 96,
+     116, 140, 168, 200, 238, 284, 336, 396, 464, 522, 576},
+    /* MPEG-2.5 8 kHz */
+    {0,   12,  24,  36,  48,  60,  72,  88,  108, 132, 160, 192,
+     232, 280, 336, 400, 476, 566, 568, 570, 572, 574, 576},
+};
+
+/* note. 0.035781 is shine_enwindow maximum value */
+/* scale and convert to fixed point before storing */
+#define SHINE_EW(x) (int32_t)((double)(x)*0x7fffffff)
+#define SHINE_EW2(a, b) SHINE_EW(a), SHINE_EW(b)
+#define SHINE_EW10(a, b, c, d, e, f, g, h, i, j)                               \
+  SHINE_EW2(a, b), SHINE_EW2(c, d), SHINE_EW2(e, f), SHINE_EW2(g, h),          \
+      SHINE_EW2(i, j)
+
+const int32_t shine_enwindow[] = {
+    SHINE_EW10(0.000000, -0.000000, -0.000000, -0.000000, -0.000000, -0.000000,
+               -0.000000, -0.000001, -0.000001, -0.000001),
+    SHINE_EW10(-0.000001, -0.000001, -0.000001, -0.000002, -0.000002, -0.000002,
+               -0.000002, -0.000003, -0.000003, -0.000003),
+    SHINE_EW10(-0.000004, -0.000004, -0.000005, -0.000005, -0.000006, -0.000007,
+               -0.000008, -0.000008, -0.000009, -0.000010),
+    SHINE_EW10(-0.000011, -0.000012, -0.000014, -0.000015, -0.000017, -0.000018,
+               -0.000020, -0.000021, -0.000023, -0.000025),
+    SHINE_EW10(-0.000028, -0.000030, -0.000032, -0.000035, -0.000038, -0.000041,
+               -0.000043, -0.000046, -0.000050, -0.000053),
+    SHINE_EW10(-0.000056, -0.000060, -0.000063, -0.000066, -0.000070, -0.000073,
+               -0.000077, -0.000081, -0.000084, -0.000087),
+    SHINE_EW10(-0.000091, -0.000093, -0.000096, -0.000099, 0.000102, 0.000104,
+               0.000106, 0.000107, 0.000108, 0.000109),
+    SHINE_EW10(0.000109, 0.000108, 0.000107, 0.000105, 0.000103, 0.000099,
+               0.000095, 0.000090, 0.000084, 0.000078),
+    SHINE_EW10(0.000070, 0.000061, 0.000051, 0.000040, 0.000027, 0.000014,
+               -0.000001, -0.000017, -0.000034, -0.000053),
+    SHINE_EW10(-0.000073, -0.000094, -0.000116, -0.000140, -0.000165, -0.000191,
+               -0.000219, -0.000247, -0.000277, -0.000308),
+    SHINE_EW10(-0.000339, -0.000371, -0.000404, -0.000438, -0.000473, -0.000507,
+               -0.000542, -0.000577, -0.000612, -0.000647),
+    SHINE_EW10(-0.000681, -0.000714, -0.000747, -0.000779, -0.000810, -0.000839,
+               -0.000866, -0.000892, -0.000915, -0.000936),
+    SHINE_EW10(-0.000954, -0.000969, -0.000981, -0.000989, -0.000994, -0.000995,
+               -0.000992, -0.000984, 0.000971, 0.000954),
+    SHINE_EW10(0.000931, 0.000903, 0.000869, 0.000829, 0.000784, 0.000732,
+               0.000674, 0.000610, 0.000539, 0.000463),
+    SHINE_EW10(0.000379, 0.000288, 0.000192, 0.000088, -0.000021, -0.000137,
+               -0.000260, -0.000388, -0.000522, -0.000662),
+    SHINE_EW10(-0.000807, -0.000957, -0.001111, -0.001270, -0.001432, -0.001598,
+               -0.001767, -0.001937, -0.002110, -0.002283),
+    SHINE_EW10(-0.002457, -0.002631, -0.002803, -0.002974, -0.003142, -0.003307,
+               -0.003467, -0.003623, -0.003772, -0.003914),
+    SHINE_EW10(-0.004049, -0.004175, -0.004291, -0.004396, -0.004490, -0.004570,
+               -0.004638, -0.004691, -0.004728, -0.004749),
+    SHINE_EW10(-0.004752, -0.004737, -0.004703, -0.004649, -0.004574, -0.004477,
+               -0.004358, -0.004215, -0.004049, -0.003859),
+    SHINE_EW10(-0.003643, -0.003402, 0.003135, 0.002841, 0.002522, 0.002175,
+               0.001801, 0.001400, 0.000971, 0.000516),
+    SHINE_EW10(0.000033, -0.000476, -0.001012, -0.001574, -0.002162, -0.002774,
+               -0.003411, -0.004072, -0.004756, -0.005462),
+    SHINE_EW10(-0.006189, -0.006937, -0.007703, -0.008487, -0.009288, -0.010104,
+               -0.010933, -0.011775, -0.012628, -0.013489),
+    SHINE_EW10(-0.014359, -0.015234, -0.016113, -0.016994, -0.017876, -0.018757,
+               -0.019634, -0.020507, -0.021372, -0.022229),
+    SHINE_EW10(-0.023074, -0.023907, -0.024725, -0.025527, -0.026311, -0.027074,
+               -0.027815, -0.028533, -0.029225, -0.029890),
+    SHINE_EW10(-0.030527, -0.031133, -0.031707, -0.032248, -0.032755, -0.033226,
+               -0.033660, -0.034056, -0.034413, -0.034730),
+    SHINE_EW10(-0.035007, -0.035242, -0.035435, -0.035586, -0.035694, -0.035759,
+               0.035781, 0.035759, 0.035694, 0.035586),
+    SHINE_EW10(0.035435, 0.035242, 0.035007, 0.034730, 0.034413, 0.034056,
+               0.033660, 0.033226, 0.032755, 0.032248),
+    SHINE_EW10(0.031707, 0.031133, 0.030527, 0.029890, 0.029225, 0.028533,
+               0.027815, 0.027074, 0.026311, 0.025527),
+    SHINE_EW10(0.024725, 0.023907, 0.023074, 0.022229, 0.021372, 0.020507,
+               0.019634, 0.018757, 0.017876, 0.016994),
+    SHINE_EW10(0.016113, 0.015234, 0.014359, 0.013489, 0.012628, 0.011775,
+               0.010933, 0.010104, 0.009288, 0.008487),
+    SHINE_EW10(0.007703, 0.006937, 0.006189, 0.005462, 0.004756, 0.004072,
+               0.003411, 0.002774, 0.002162, 0.001574),
+    SHINE_EW10(0.001012, 0.000476, -0.000033, -0.000516, -0.000971, -0.001400,
+               -0.001801, -0.002175, -0.002522, -0.002841),
+    SHINE_EW10(0.003135, 0.003402, 0.003643, 0.003859, 0.004049, 0.004215,
+               0.004358, 0.004477, 0.004574, 0.004649),
+    SHINE_EW10(0.004703, 0.004737, 0.004752, 0.004749, 0.004728, 0.004691,
+               0.004638, 0.004570, 0.004490, 0.004396),
+    SHINE_EW10(0.004291, 0.004175, 0.004049, 0.003914, 0.003772, 0.003623,
+               0.003467, 0.003307, 0.003142, 0.002974),
+    SHINE_EW10(0.002803, 0.002631, 0.002457, 0.002283, 0.002110, 0.001937,
+               0.001767, 0.001598, 0.001432, 0.001270),
+    SHINE_EW10(0.001111, 0.000957, 0.000807, 0.000662, 0.000522, 0.000388,
+               0.000260, 0.000137, 0.000021, -0.000088),
+    SHINE_EW10(-0.000192, -0.000288, -0.000379, -0.000463, -0.000539, -0.000610,
+               -0.000674, -0.000732, -0.000784, -0.000829),
+    SHINE_EW10(-0.000869, -0.000903, -0.000931, -0.000954, 0.000971, 0.000984,
+               0.000992, 0.000995, 0.000994, 0.000989),
+    SHINE_EW10(0.000981, 0.000969, 0.000954, 0.000936, 0.000915, 0.000892,
+               0.000866, 0.000839, 0.000810, 0.000779),
+    SHINE_EW10(0.000747, 0.000714, 0.000681, 0.000647, 0.000612, 0.000577,
+               0.000542, 0.000507, 0.000473, 0.000438),
+    SHINE_EW10(0.000404, 0.000371, 0.000339, 0.000308, 0.000277, 0.000247,
+               0.000219, 0.000191, 0.000165, 0.000140),
+    SHINE_EW10(0.000116, 0.000094, 0.000073, 0.000053, 0.000034, 0.000017,
+               0.000001, -0.000014, -0.000027, -0.000040),
+    SHINE_EW10(-0.000051, -0.000061, -0.000070, -0.000078, -0.000084, -0.000090,
+               -0.000095, -0.000099, -0.000103, -0.000105),
+    SHINE_EW10(-0.000107, -0.000108, -0.000109, -0.000109, -0.000108, -0.000107,
+               -0.000106, -0.000104, 0.000102, 0.000099),
+    SHINE_EW10(0.000096, 0.000093, 0.000091, 0.000087, 0.000084, 0.000081,
+               0.000077, 0.000073, 0.000070, 0.000066),
+    SHINE_EW10(0.000063, 0.000060, 0.000056, 0.000053, 0.000050, 0.000046,
+               0.000043, 0.000041, 0.000038, 0.000035),
+    SHINE_EW10(0.000032, 0.000030, 0.000028, 0.000025, 0.000023, 0.000021,
+               0.000020, 0.000018, 0.000017, 0.000015),
+    SHINE_EW10(0.000014, 0.000012, 0.000011, 0.000010, 0.000009, 0.000008,
+               0.000008, 0.000007, 0.000006, 0.000005),
+    SHINE_EW10(0.000005, 0.000004, 0.000004, 0.000003, 0.000003, 0.000003,
+               0.000002, 0.000002, 0.000002, 0.000002),
+    SHINE_EW10(0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001,
+               0.000000, 0.000000, 0.000000, 0.000000),
+    SHINE_EW2(0.000000, 0.000000)};

+ 15 - 0
lib/shine/src/tables.h

@@ -0,0 +1,15 @@
+#ifndef TABLES_H
+#define TABLES_H
+
+#include "types.h"
+
+extern const int shine_slen1_tab[16];
+extern const int shine_slen2_tab[16];
+
+extern const int samplerates[9];
+extern const int bitrates[16][4];
+
+extern const int shine_scale_fact_band_index[9][23];
+extern const int32_t shine_enwindow[];
+
+#endif

+ 180 - 0
lib/shine/src/types.h

@@ -0,0 +1,180 @@
+#ifndef PRIV_TYPES_H
+#define PRIV_TYPES_H
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <math.h>
+
+#define GRANULE_SIZE 576
+
+#include "bitstream.h"
+
+/* Include arch-specific instructions,
+ * when defined. */
+#if defined(__mips__) && (__mips == 32)
+#include "mult_mips_gcc.h"
+#elif defined(__arm__) && !defined(__thumb__)
+#include "mult_sarm_gcc.h"
+#endif
+
+/* Include and define generic instructions,
+ * when not already defined above. */
+#include "mult_noarch_gcc.h"
+
+#ifndef SWAB32
+#if defined(__GNUC__) &&                                                       \
+    (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 2))
+#define SWAB32(x) __builtin_bswap32(x)
+#else
+#define SWAB32(x)                                                              \
+  (((unsigned int)(x) >> 24) | (((unsigned int)(x) >> 8) & 0xff00) |           \
+   (((unsigned int)(x)&0xff00) << 8) | ((unsigned int)(x) << 24))
+#endif
+#endif
+
+/* #define DEBUG if you want the library to dump info to stdout */
+
+#define PI 3.14159265358979
+#define PI4 0.78539816339745
+#define PI12 0.26179938779915
+#define PI36 0.087266462599717
+#define PI64 0.049087385212
+#define SQRT2 1.41421356237
+#define LN2 0.69314718
+#define LN_TO_LOG10 0.2302585093
+#define BLKSIZE 1024
+#define HAN_SIZE 512 /* for loop unrolling, require that HAN_SIZE%8==0 */
+#define SCALE_BLOCK 12
+#define SCALE_RANGE 64
+#define SCALE 32768
+#define SBLIMIT 32
+
+#ifndef MAX_CHANNELS
+#define MAX_CHANNELS 2
+#endif
+
+#ifndef MAX_GRANULES
+#define MAX_GRANULES 2
+#endif
+
+typedef struct {
+  int channels;
+  int samplerate;
+} priv_shine_wave_t;
+
+typedef struct {
+  int version;
+  int layer;
+  int granules_per_frame;
+  int mode; /* + */ /* Stereo mode */
+  int bitr; /* + */ /* Must conform to known bitrate - see Main.c */
+  int emph; /* + */ /* De-emphasis */
+  int padding;
+  int bits_per_frame;
+  int bits_per_slot;
+  double frac_slots_per_frame;
+  double slot_lag;
+  int whole_slots_per_frame;
+  int bitrate_index; /* + */    /* See Main.c and Layer3.c */
+  int samplerate_index; /* + */ /* See Main.c and Layer3.c */
+  int crc;
+  int ext;
+  int mode_ext;
+  int copyright; /* + */
+  int original;  /* + */
+} priv_shine_mpeg_t;
+
+typedef struct {
+  int32_t *xr;                  /* magnitudes of the spectral values */
+  int32_t xrsq[GRANULE_SIZE];   /* xr squared */
+  int32_t xrabs[GRANULE_SIZE];  /* xr absolute */
+  int32_t xrmax;                /* maximum of xrabs array */
+  int32_t en_tot[MAX_GRANULES]; /* gr */
+  int32_t en[MAX_GRANULES][21];
+  int32_t xm[MAX_GRANULES][21];
+  int32_t xrmaxl[MAX_GRANULES];
+  double steptab[128];   /* 2**(-x/4)  for x = -127..0 */
+  int32_t steptabi[128]; /* 2**(-x/4)  for x = -127..0 */
+  int int2idx[10000];    /* x**(3/4)   for x = 0..9999 */
+} l3loop_t;
+
+typedef struct {
+  int32_t cos_l[18][36];
+} mdct_t;
+
+typedef struct {
+  int off[MAX_CHANNELS];
+  int32_t fl[SBLIMIT][64];
+  int32_t x[MAX_CHANNELS][HAN_SIZE];
+} subband_t;
+
+/* Side information */
+typedef struct {
+  unsigned part2_3_length;
+  unsigned big_values;
+  unsigned count1;
+  unsigned global_gain;
+  unsigned scalefac_compress;
+  unsigned table_select[3];
+  unsigned region0_count;
+  unsigned region1_count;
+  unsigned preflag;
+  unsigned scalefac_scale;
+  unsigned count1table_select;
+  unsigned part2_length;
+  unsigned sfb_lmax;
+  unsigned address1;
+  unsigned address2;
+  unsigned address3;
+  int quantizerStepSize;
+  unsigned slen[4];
+} gr_info;
+
+typedef struct {
+  unsigned private_bits;
+  int resvDrain;
+  unsigned scfsi[MAX_CHANNELS][4];
+  struct {
+    struct {
+      gr_info tt;
+    } ch[MAX_CHANNELS];
+  } gr[MAX_GRANULES];
+} shine_side_info_t;
+
+typedef struct {
+  double l[MAX_GRANULES][MAX_CHANNELS][21];
+} shine_psy_ratio_t;
+
+typedef struct {
+  double l[MAX_GRANULES][MAX_CHANNELS][21];
+} shine_psy_xmin_t;
+
+typedef struct {
+  int32_t l[MAX_GRANULES][MAX_CHANNELS][22];    /* [cb] */
+  int32_t s[MAX_GRANULES][MAX_CHANNELS][13][3]; /* [window][cb] */
+} shine_scalefac_t;
+
+typedef struct shine_global_flags {
+  priv_shine_wave_t wave;
+  priv_shine_mpeg_t mpeg;
+  bitstream_t bs;
+  shine_side_info_t side_info;
+  int sideinfo_len;
+  int mean_bits;
+  shine_psy_ratio_t ratio;
+  shine_scalefac_t scalefactor;
+  int16_t *buffer[MAX_CHANNELS];
+  double pe[MAX_CHANNELS][MAX_GRANULES];
+  int l3_enc[MAX_CHANNELS][MAX_GRANULES][GRANULE_SIZE];
+  int32_t l3_sb_sample[MAX_CHANNELS][MAX_GRANULES + 1][18][SBLIMIT];
+  int32_t mdct_freq[MAX_CHANNELS][MAX_GRANULES][GRANULE_SIZE];
+  int ResvSize;
+  int ResvMax;
+  l3loop_t l3loop;
+  mdct_t mdct;
+  subband_t subband;
+} shine_global_config;
+
+#endif

+ 5 - 1
platformio.ini

@@ -6,7 +6,11 @@
 ;   pio device monitor   serial log at 115200
 
 [env:esp32-s3-rlcd-42]
-platform = espressif32
+; Pinned. src/audio.cpp uses the legacy driver/i2s.h API, which arduino-esp32
+; 3.x (IDF 5) removed in favour of driver/i2s_std.h. Floating this version
+; would break the microphone capture on the next platform release, so the
+; upgrade needs to be a deliberate edit to audio.cpp, not a surprise.
+platform = espressif32@6.5.0
 board = esp32-s3-devkitc-1
 framework = arduino
 

+ 687 - 0
src/audio.cpp

@@ -0,0 +1,687 @@
+#include "audio.h"
+
+#include <FS.h>
+#include <SD_MMC.h>
+#include <WiFi.h>
+#include <driver/i2s.h>
+#include <esp_heap_caps.h>
+#include <freertos/FreeRTOS.h>
+#include <freertos/stream_buffer.h>
+#include <freertos/task.h>
+
+extern "C" {
+#include "layer3.h"  // lib/shine
+}
+
+#include "config.h"
+#include "i2c_bus.h"
+
+namespace {
+
+// ------------------------------------------------------------- ES7210 regs ---
+// Register map and the initialisation order below follow ESPHome's es7210
+// component, which in turn follows Espressif's esp-adf driver. The magic
+// numbers are the vendor's, not ours; they are reproduced rather than derived
+// because the datasheet does not explain most of them.
+constexpr uint8_t REG_RESET = 0x00;
+constexpr uint8_t REG_CLOCK_OFF = 0x01;
+constexpr uint8_t REG_MAINCLK = 0x02;
+constexpr uint8_t REG_LRCK_DIVH = 0x04;
+constexpr uint8_t REG_LRCK_DIVL = 0x05;
+constexpr uint8_t REG_POWER_DOWN = 0x06;
+constexpr uint8_t REG_OSR = 0x07;
+constexpr uint8_t REG_MODE_CONFIG = 0x08;
+constexpr uint8_t REG_TIME_CONTROL0 = 0x09;
+constexpr uint8_t REG_TIME_CONTROL1 = 0x0A;
+constexpr uint8_t REG_SDP_INTERFACE1 = 0x11;
+constexpr uint8_t REG_SDP_INTERFACE2 = 0x12;
+constexpr uint8_t REG_ADC34_HPF2 = 0x20;
+constexpr uint8_t REG_ADC34_HPF1 = 0x21;
+constexpr uint8_t REG_ADC12_HPF1 = 0x22;
+constexpr uint8_t REG_ADC12_HPF2 = 0x23;
+constexpr uint8_t REG_ANALOG = 0x40;
+constexpr uint8_t REG_MIC12_BIAS = 0x41;
+constexpr uint8_t REG_MIC34_BIAS = 0x42;
+constexpr uint8_t REG_MIC1_GAIN = 0x43;
+constexpr uint8_t REG_MIC2_GAIN = 0x44;
+constexpr uint8_t REG_MIC3_GAIN = 0x45;
+constexpr uint8_t REG_MIC4_GAIN = 0x46;
+constexpr uint8_t REG_MIC1_POWER = 0x47;
+constexpr uint8_t REG_MIC12_POWER = 0x4B;
+constexpr uint8_t REG_MIC34_POWER = 0x4C;
+
+// Coefficients for 16 kHz with MCLK at 256 x Fs (4.096 MHz), the one rate this
+// firmware records at. ESPHome carries the full 28-row table; only this row is
+// reachable here, so only this row is copied.
+constexpr uint8_t COEF_ADC_DIV = 0x01;
+constexpr uint8_t COEF_DOUBLER = 0x01;
+constexpr uint8_t COEF_DLL = 0x01;
+constexpr uint8_t COEF_OSR = 0x20;
+constexpr uint8_t COEF_LRCK_H = 0x01;
+constexpr uint8_t COEF_LRCK_L = 0x00;
+
+constexpr i2s_port_t I2S_PORT = I2S_NUM_0;
+
+// ------------------------------------------------------------------ state ---
+bool g_available = false;
+bool g_sdMounted = false;
+
+volatile audio::State g_state = audio::State::Idle;
+audio::Destination g_dest = audio::Destination::Psram;
+bool g_stereo = AUDIO_STEREO_DEFAULT;
+char g_name[32] = "";
+char g_error[80] = "";
+
+uint8_t *g_psram = nullptr;
+uint32_t g_psramSize = 0;
+
+File g_file;
+WiFiClient g_streamClient;
+StreamBufferHandle_t g_ring = nullptr;
+
+TaskHandle_t g_task = nullptr;
+volatile bool g_stopRequested = false;
+volatile bool g_finished = false;
+
+uint32_t g_startMs = 0;
+volatile uint32_t g_elapsedMs = 0;
+volatile uint32_t g_encodedBytes = 0;
+volatile float g_peak = 0.0f;
+
+void setError(const char *msg)
+{
+  strlcpy(g_error, msg, sizeof(g_error));
+  Serial.printf("[audio] %s\n", msg);
+}
+
+// ----------------------------------------------------------- i2c helpers ---
+bool regWrite(uint8_t reg, uint8_t value)
+{
+  Wire.beginTransmission(ES7210_ADDR);
+  Wire.write(reg);
+  Wire.write(value);
+  return Wire.endTransmission() == 0;
+}
+
+bool regRead(uint8_t reg, uint8_t &value)
+{
+  Wire.beginTransmission(ES7210_ADDR);
+  Wire.write(reg);
+  if (Wire.endTransmission(false) != 0) {
+    return false;
+  }
+  if (Wire.requestFrom((uint8_t)ES7210_ADDR, (uint8_t)1) != 1) {
+    return false;
+  }
+  value = Wire.read();
+  return true;
+}
+
+// Read-modify-write of selected bits, the es7210_update_reg_bit_ helper.
+bool regUpdate(uint8_t reg, uint8_t mask, uint8_t data)
+{
+  uint8_t v = 0;
+  if (!regRead(reg, v)) {
+    return false;
+  }
+  v = (v & ~mask) | (mask & data);
+  return regWrite(reg, v);
+}
+
+// ---------------------------------------------------------- ES7210 bring-up ---
+// Gain is 0-14 in 3 dB steps, applied to all four channels. Only MIC1 and MIC2
+// are fitted on this board, but the vendor sequence powers and gains all four
+// and deviating from it is not worth the risk.
+bool es7210Begin()
+{
+  const uint8_t gain = AUDIO_MIC_GAIN > 14 ? 14 : AUDIO_MIC_GAIN;
+
+  bool ok = true;
+  ok &= regWrite(REG_RESET, 0xff);
+  ok &= regWrite(REG_RESET, 0x32);
+  ok &= regWrite(REG_CLOCK_OFF, 0x3f);
+
+  // Power-up settling time.
+  ok &= regWrite(REG_TIME_CONTROL0, 0x30);
+  ok &= regWrite(REG_TIME_CONTROL1, 0x30);
+
+  // High-pass filters on every ADC channel, which is what removes the DC
+  // offset the MEMS capsules sit at.
+  ok &= regWrite(REG_ADC12_HPF2, 0x2a);
+  ok &= regWrite(REG_ADC12_HPF1, 0x0a);
+  ok &= regWrite(REG_ADC34_HPF2, 0x0a);
+  ok &= regWrite(REG_ADC34_HPF1, 0x2a);
+
+  // Slave mode: the ESP32 supplies MCLK, BCLK and LRCLK.
+  ok &= regUpdate(REG_MODE_CONFIG, 0x01, 0x00);
+
+  ok &= regWrite(REG_ANALOG, 0xC3);
+  ok &= regWrite(REG_MIC12_BIAS, 0x70);
+  ok &= regWrite(REG_MIC34_BIAS, 0x70);
+
+  // Format: 16-bit, standard I2S rather than TDM.
+  ok &= regWrite(REG_SDP_INTERFACE1, 0x60);
+  ok &= regWrite(REG_SDP_INTERFACE2, 0x00);
+
+  // Sample rate, from the 16 kHz coefficient row.
+  ok &= regWrite(REG_MAINCLK,
+                 (uint8_t)(COEF_ADC_DIV | (COEF_DOUBLER << 6) | (COEF_DLL << 7)));
+  ok &= regWrite(REG_OSR, COEF_OSR);
+  ok &= regWrite(REG_LRCK_DIVH, COEF_LRCK_H);
+  ok &= regWrite(REG_LRCK_DIVL, COEF_LRCK_L);
+
+  // Gain. The vendor sequence toggles the channel out of and back into its
+  // powered state around each write, so it is reproduced per channel.
+  for (uint8_t i = 0; i < 4; ++i) {
+    ok &= regUpdate((uint8_t)(REG_MIC1_GAIN + i), 0x10, 0x00);
+  }
+  ok &= regWrite(REG_MIC12_POWER, 0xff);
+  ok &= regWrite(REG_MIC34_POWER, 0xff);
+
+  const uint8_t gainRegs[4] = {REG_MIC1_GAIN, REG_MIC2_GAIN, REG_MIC3_GAIN,
+                               REG_MIC4_GAIN};
+  for (uint8_t i = 0; i < 4; ++i) {
+    const bool upper = i >= 2;  // MIC3/4 live in the other clock domain
+    ok &= regUpdate(REG_CLOCK_OFF, upper ? 0x15 : 0x0b, 0x00);
+    ok &= regWrite(upper ? REG_MIC34_POWER : REG_MIC12_POWER, 0x00);
+    ok &= regUpdate(gainRegs[i], 0x10, 0x10);
+    ok &= regUpdate(gainRegs[i], 0x0f, gain);
+  }
+
+  for (uint8_t i = 0; i < 4; ++i) {
+    ok &= regWrite((uint8_t)(REG_MIC1_POWER + i), 0x08);
+  }
+  ok &= regWrite(REG_POWER_DOWN, 0x04);
+  ok &= regWrite(REG_MIC12_POWER, 0x0F);
+  ok &= regWrite(REG_MIC34_POWER, 0x0F);
+
+  ok &= regWrite(REG_RESET, 0x71);
+  ok &= regWrite(REG_RESET, 0x41);
+
+  return ok;
+}
+
+void es7210End()
+{
+  // Back to the reset state so the analog front end stops drawing current.
+  regWrite(REG_MIC12_POWER, 0xff);
+  regWrite(REG_MIC34_POWER, 0xff);
+  regWrite(REG_CLOCK_OFF, 0x3f);
+  regWrite(REG_RESET, 0x32);
+}
+
+// --------------------------------------------------------------------- i2s ---
+bool i2sBegin()
+{
+  i2s_config_t cfg = {};
+  cfg.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX);
+  cfg.sample_rate = AUDIO_SAMPLE_RATE;
+  cfg.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT;
+  cfg.channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT;  // both microphones
+  cfg.communication_format = I2S_COMM_FORMAT_STAND_I2S;
+  cfg.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1;
+  cfg.dma_buf_count = 8;
+  cfg.dma_buf_len = 256;
+  cfg.use_apll = false;
+  cfg.tx_desc_auto_clear = false;
+  cfg.fixed_mclk = 0;
+  // The ES7210 coefficients above assume MCLK is exactly 256 x Fs.
+  cfg.mclk_multiple = I2S_MCLK_MULTIPLE_256;
+  cfg.bits_per_chan = I2S_BITS_PER_CHAN_DEFAULT;
+
+  if (i2s_driver_install(I2S_PORT, &cfg, 0, nullptr) != ESP_OK) {
+    setError("I2S driver install failed");
+    return false;
+  }
+
+  i2s_pin_config_t pins = {};
+  pins.mck_io_num = I2S_MCLK_PIN;
+  pins.bck_io_num = I2S_BCLK_PIN;
+  pins.ws_io_num = I2S_LRCLK_PIN;
+  pins.data_out_num = I2S_PIN_NO_CHANGE;  // playback is not built
+  pins.data_in_num = ES7210_DIN_PIN;
+
+  if (i2s_set_pin(I2S_PORT, &pins) != ESP_OK) {
+    i2s_driver_uninstall(I2S_PORT);
+    setError("I2S pin config failed");
+    return false;
+  }
+
+  i2s_zero_dma_buffer(I2S_PORT);
+  return true;
+}
+
+void i2sEnd()
+{
+  i2s_driver_uninstall(I2S_PORT);
+}
+
+// ---------------------------------------------------------------- sinks ---
+// Each destination consumes encoded MP3 the moment a frame is produced.
+// Returns false when the destination is full or gone, which ends the
+// recording.
+bool sinkWrite(const uint8_t *data, uint32_t len)
+{
+  switch (g_dest) {
+    case audio::Destination::Psram:
+      if (g_psramSize + len > AUDIO_PSRAM_MAX_BYTES) {
+        return false;  // the cap, not an error
+      }
+      memcpy(g_psram + g_psramSize, data, len);
+      g_psramSize += len;
+      return true;
+
+    case audio::Destination::Sd:
+      return g_file && g_file.write(data, len) == len;
+
+    case audio::Destination::Stream:
+      // Never block the encoder on a slow client: overflow is dropped audio,
+      // a stalled encoder is an overrun of the I2S DMA and a corrupt file.
+      xStreamBufferSend(g_ring, data, len, 0);
+      return true;
+  }
+  return false;
+}
+
+// -------------------------------------------------------------- recorder ---
+void recorderTask(void *)
+{
+  shine_config_t cfg;
+  shine_set_config_mpeg_defaults(&cfg.mpeg);
+  cfg.wave.channels = g_stereo ? PCM_STEREO : PCM_MONO;
+  cfg.wave.samplerate = AUDIO_SAMPLE_RATE;
+  cfg.mpeg.mode = g_stereo ? JOINT_STEREO : MONO;
+  cfg.mpeg.bitr = g_stereo ? AUDIO_BITRATE_KBPS * 2 : AUDIO_BITRATE_KBPS;
+
+  shine_t enc = shine_initialise(&cfg);
+  if (enc == nullptr) {
+    setError("MP3 encoder init failed");
+    g_finished = true;
+    g_task = nullptr;
+    vTaskDelete(nullptr);
+    return;
+  }
+
+  const int samplesPerPass = shine_samples_per_pass(enc);
+
+  // The ES7210 always sends two channels; mono is the average of the pair.
+  const size_t frameBytes = (size_t)samplesPerPass * 2 * sizeof(int16_t);
+  int16_t *raw = (int16_t *)malloc(frameBytes);
+  int16_t *mono = g_stereo ? nullptr
+                           : (int16_t *)malloc((size_t)samplesPerPass * sizeof(int16_t));
+
+  if (raw == nullptr || (!g_stereo && mono == nullptr)) {
+    setError("out of memory for capture buffers");
+    free(raw);
+    free(mono);
+    shine_close(enc);
+    g_finished = true;
+    g_task = nullptr;
+    vTaskDelete(nullptr);
+    return;
+  }
+
+  while (!g_stopRequested) {
+    size_t got = 0;
+    // A full frame is ~36 ms, so this timeout only bites when the codec has
+    // gone quiet. Kept short because stop() waits for this loop to notice the
+    // flag, and loop() is waiting on stop().
+    const esp_err_t err =
+        i2s_read(I2S_PORT, raw, frameBytes, &got, pdMS_TO_TICKS(100));
+    if (err != ESP_OK || got < frameBytes) {
+      continue;  // a short read is a timeout, not a failure
+    }
+
+    // Peak over the block, for the level meter on the page.
+    int32_t peak = 0;
+    const size_t sampleCount = frameBytes / sizeof(int16_t);
+    for (size_t i = 0; i < sampleCount; ++i) {
+      const int32_t v = raw[i] < 0 ? -(int32_t)raw[i] : raw[i];
+      if (v > peak) {
+        peak = v;
+      }
+    }
+    g_peak = (float)peak / 32768.0f;
+
+    int written = 0;
+    unsigned char *out = nullptr;
+    if (g_stereo) {
+      out = shine_encode_buffer_interleaved(enc, raw, &written);
+    } else {
+      for (int i = 0; i < samplesPerPass; ++i) {
+        mono[i] = (int16_t)(((int32_t)raw[i * 2] + raw[i * 2 + 1]) / 2);
+      }
+      int16_t *channels[2] = {mono, nullptr};
+      out = shine_encode_buffer(enc, channels, &written);
+    }
+
+    if (written > 0) {
+      if (!sinkWrite(out, (uint32_t)written)) {
+        break;
+      }
+      g_encodedBytes += (uint32_t)written;
+    }
+    g_elapsedMs = millis() - g_startMs;
+  }
+
+  // Trailing partial frame.
+  int written = 0;
+  unsigned char *out = shine_flush(enc, &written);
+  if (written > 0 && sinkWrite(out, (uint32_t)written)) {
+    g_encodedBytes += (uint32_t)written;
+  }
+
+  shine_close(enc);
+  free(raw);
+  free(mono);
+
+  g_elapsedMs = millis() - g_startMs;
+  g_finished = true;
+  g_task = nullptr;
+  vTaskDelete(nullptr);
+}
+
+// Tears down everything the recording held. Runs on the main task, so the CPU
+// frequency change never races the encoder.
+void finishIfDone()
+{
+  if (!g_finished || g_state != audio::State::Recording) {
+    return;
+  }
+  g_finished = false;
+
+  i2sEnd();
+  es7210End();
+
+  if (g_file) {
+    g_file.close();
+  }
+
+  if (g_dest == audio::Destination::Psram && g_psram != nullptr) {
+    // The buffer was reserved at the full cap; hand the unused tail back
+    // rather than sitting on megabytes of PSRAM until the next recording.
+    if (g_psramSize == 0) {
+      audio::clearPsram();
+    } else {
+      uint8_t *shrunk =
+          (uint8_t *)heap_caps_realloc(g_psram, g_psramSize, MALLOC_CAP_SPIRAM);
+      if (shrunk != nullptr) {
+        g_psram = shrunk;
+      }
+    }
+  }
+
+  if (g_dest == audio::Destination::Stream) {
+    // Push whatever is still buffered before dropping the connection.
+    // Static: this only ever runs on the main task, and the Arduino loop
+    // stack is not the place for a 1.5 kB frame.
+    static uint8_t tail[AUDIO_STREAM_CHUNK];
+    while (g_streamClient.connected()) {
+      const size_t n = xStreamBufferReceive(g_ring, tail, sizeof(tail), 0);
+      if (n == 0) {
+        break;
+      }
+      g_streamClient.write(tail, n);
+    }
+    g_streamClient.stop();
+  }
+  if (g_ring != nullptr) {
+    vStreamBufferDelete(g_ring);
+    g_ring = nullptr;
+  }
+
+  setCpuFrequencyMhz(CPU_FREQ_MHZ);
+  g_state = g_error[0] != '\0' ? audio::State::Failed : audio::State::Idle;
+
+  Serial.printf("[audio] stopped: %u bytes in %u ms\n",
+                (unsigned)g_encodedBytes, (unsigned)g_elapsedMs);
+}
+
+}  // namespace
+
+namespace audio {
+
+void begin()
+{
+  // The speaker amplifier shares this bus. Nothing here drives it, and leaving
+  // it enabled would burn current and risk feedback into the microphones.
+  pinMode(SPEAKER_PA_PIN, OUTPUT);
+  digitalWrite(SPEAKER_PA_PIN, LOW);
+
+  if (!i2cBusBegin()) {
+    setError("I2C bus unavailable");
+    return;
+  }
+
+  g_available = i2cDevicePresent(ES7210_ADDR);
+  if (!g_available) {
+    setError("ES7210 not found");
+    return;
+  }
+
+  SD_MMC.setPins(SD_CLK_PIN, SD_CMD_PIN, SD_D0_PIN);
+  // 1-bit mode: only D0 is wired on this board.
+  g_sdMounted = SD_MMC.begin("/sdcard", true);
+
+  Serial.printf("[audio] ES7210 ready, SD card %s\n",
+                g_sdMounted ? "mounted" : "absent");
+}
+
+bool isAvailable()
+{
+  return g_available;
+}
+
+bool hasSdCard()
+{
+  return g_sdMounted;
+}
+
+bool start(Destination dest, bool stereo, const char *name)
+{
+  if (!g_available) {
+    setError("no microphone codec");
+    return false;
+  }
+  if (g_state == State::Recording) {
+    return false;
+  }
+
+  g_error[0] = '\0';
+  g_dest = dest;
+  g_stereo = stereo;
+  g_encodedBytes = 0;
+  g_elapsedMs = 0;
+  g_peak = 0.0f;
+  g_stopRequested = false;
+  g_finished = false;
+
+  if (name != nullptr && name[0] != '\0') {
+    strlcpy(g_name, name, sizeof(g_name));
+  } else {
+    snprintf(g_name, sizeof(g_name), "rec-%lu", (unsigned long)(millis() / 1000));
+  }
+
+  switch (dest) {
+    case Destination::Psram: {
+      clearPsram();
+      g_psram = (uint8_t *)heap_caps_malloc(AUDIO_PSRAM_MAX_BYTES, MALLOC_CAP_SPIRAM);
+      if (g_psram == nullptr) {
+        setError("could not reserve PSRAM");
+        return false;
+      }
+      g_psramSize = 0;
+      break;
+    }
+    case Destination::Sd: {
+      if (!g_sdMounted) {
+        setError("no SD card");
+        return false;
+      }
+      char path[64];
+      snprintf(path, sizeof(path), "/%s.mp3", g_name);
+      g_file = SD_MMC.open(path, FILE_WRITE);
+      if (!g_file) {
+        setError("could not open file on SD card");
+        return false;
+      }
+      break;
+    }
+    case Destination::Stream: {
+      if (!g_streamClient.connected()) {
+        setError("no listener connected");
+        return false;
+      }
+      g_ring = xStreamBufferCreate(AUDIO_RING_BYTES, 1);
+      if (g_ring == nullptr) {
+        setError("could not allocate stream buffer");
+        return false;
+      }
+      break;
+    }
+  }
+
+  // Shine cannot encode in real time at CPU_FREQ_MHZ. Raised before the I2S
+  // driver is installed so its clock dividers are computed at the final rate.
+  setCpuFrequencyMhz(AUDIO_CPU_FREQ_MHZ);
+
+  if (!es7210Begin()) {
+    setError("ES7210 configuration failed");
+    setCpuFrequencyMhz(CPU_FREQ_MHZ);
+    return false;
+  }
+  if (!i2sBegin()) {
+    es7210End();
+    setCpuFrequencyMhz(CPU_FREQ_MHZ);
+    return false;
+  }
+
+  g_startMs = millis();
+  g_state = State::Recording;
+
+  // Core 1 keeps the encoder away from the Wi-Fi stack on core 0, and off the
+  // Arduino loop task so the clock carries on updating while recording.
+  if (xTaskCreatePinnedToCore(recorderTask, "audio_rec", 8192, nullptr, 5,
+                              &g_task, 1) != pdPASS) {
+    setError("could not start recorder task");
+    i2sEnd();
+    es7210End();
+    setCpuFrequencyMhz(CPU_FREQ_MHZ);
+    g_state = State::Failed;
+    return false;
+  }
+
+  Serial.printf("[audio] recording '%s' %s to %s\n", g_name,
+                stereo ? "stereo" : "mono",
+                dest == Destination::Psram ? "PSRAM"
+                    : dest == Destination::Sd ? "SD card" : "stream");
+  return true;
+}
+
+void stop()
+{
+  if (g_state != State::Recording) {
+    return;
+  }
+  g_stopRequested = true;
+
+  // The task only needs to finish the frame it is on.
+  const uint32_t deadline = millis() + 3000;
+  while (!g_finished && (int32_t)(millis() - deadline) < 0) {
+    delay(10);
+  }
+  if (!g_finished) {
+    setError("recorder task did not stop");
+    g_finished = true;
+  }
+  finishIfDone();
+}
+
+void loop()
+{
+  if (g_state == State::Recording && g_dest == Destination::Stream) {
+    if (!g_streamClient.connected()) {
+      // The listener navigated away; there is nowhere left to put the audio.
+      stop();
+      return;
+    }
+
+    // Bounded by what the socket can take right now, so this never waits on
+    // the network — loop() still has a clock to update.
+    int room = g_streamClient.availableForWrite();
+    if (room > AUDIO_STREAM_CHUNK) {
+      room = AUDIO_STREAM_CHUNK;
+    }
+    if (room > 0) {
+      static uint8_t buf[AUDIO_STREAM_CHUNK];
+      const size_t n = xStreamBufferReceive(g_ring, buf, (size_t)room, 0);
+      if (n > 0) {
+        g_streamClient.write(buf, n);
+      }
+    }
+  }
+
+  finishIfDone();
+}
+
+void attachStreamClient(WiFiClient client)
+{
+  g_streamClient.stop();
+  g_streamClient = client;
+}
+
+State state()
+{
+  return g_state;
+}
+
+bool isRecording()
+{
+  return g_state == State::Recording;
+}
+
+uint32_t elapsedMs()
+{
+  return g_elapsedMs;
+}
+
+uint32_t encodedBytes()
+{
+  return g_encodedBytes;
+}
+
+float peakLevel()
+{
+  return g_peak;
+}
+
+const char *lastError()
+{
+  return g_error;
+}
+
+const uint8_t *psramData()
+{
+  return g_psramSize > 0 ? g_psram : nullptr;
+}
+
+uint32_t psramSize()
+{
+  return g_psramSize;
+}
+
+void clearPsram()
+{
+  if (g_psram != nullptr) {
+    heap_caps_free(g_psram);
+    g_psram = nullptr;
+  }
+  g_psramSize = 0;
+}
+
+const char *recordingName()
+{
+  return g_name;
+}
+
+}  // namespace audio

+ 101 - 0
src/audio.h

@@ -0,0 +1,101 @@
+#pragma once
+
+#include <Arduino.h>
+#include <WiFiClient.h>
+
+// Microphone capture and MP3 encoding.
+//
+// The two onboard MEMS microphones are analog and feed an ES7210 ADC, which
+// hands the ESP32 a two-channel I2S stream. Raw 16-bit PCM is 32 kB/s, so it is
+// encoded to MP3 (lib/shine) as it arrives rather than buffered raw.
+//
+// Capture and encoding run in their own FreeRTOS task, pinned to core 1. This
+// is deliberate: it is what lets the clock keep ticking through a recording,
+// and it is the same reason nothing here blocks in loop(). The task produces
+// MP3 frames into a ring buffer; the destination drains it.
+//
+// Three destinations, chosen when the recording starts:
+//
+//   Psram   — accumulate in PSRAM, download when finished. Capped at
+//             AUDIO_PSRAM_MAX_BYTES. Lost on reboot.
+//   Sd      — append to a file on the microSD card. Survives a reboot and is
+//             limited only by the card.
+//   Stream  — push to an HTTP client as it is encoded. Nothing is retained.
+//
+// MP3 encoding cannot keep up at CPU_FREQ_MHZ, so begin()/end() of a recording
+// also raise and restore the CPU clock.
+namespace audio {
+
+enum class Destination : uint8_t {
+  Psram = 0,
+  Sd = 1,
+  Stream = 2,
+};
+
+enum class State : uint8_t {
+  Idle = 0,
+  Recording = 1,
+  Failed = 2,
+};
+
+// Probes the ES7210 over I2C and notes whether an SD card is present. Does not
+// install the I2S driver or power anything up — that happens on start(). Safe
+// to call when no codec is fitted; every entry point below then fails politely.
+void begin();
+
+// True if the ES7210 answered at boot. False means the feature is unavailable
+// and the page says so rather than offering a dead button.
+bool isAvailable();
+
+// True if a usable microSD card was mounted.
+bool hasSdCard();
+
+// Starts a recording. `stereo` keeps MIC1 and MIC2 as left and right; otherwise
+// the two are averaged to one channel at half the bitrate. `name` is used for
+// the SD filename and the download filename; a default is chosen if empty.
+// Returns false if already recording, or if the destination is unusable.
+bool start(Destination dest, bool stereo, const char *name);
+
+// Stops the current recording, flushes the encoder and restores the CPU clock.
+// Harmless when idle.
+void stop();
+
+// Services the streaming destination: moves encoded bytes from the ring buffer
+// to the waiting HTTP client, at most AUDIO_STREAM_CHUNK per call. Must be
+// called from loop(). Does nothing for the other destinations.
+void loop();
+
+// Hands a connected client to the streaming destination. The caller has already
+// sent the response headers. Ownership moves here; the client is closed when
+// the recording stops or the peer goes away.
+void attachStreamClient(WiFiClient client);
+
+State state();
+bool isRecording();
+
+// Milliseconds since the current recording started, or the length of the last
+// one when idle.
+uint32_t elapsedMs();
+
+// Bytes of MP3 produced by the current or last recording.
+uint32_t encodedBytes();
+
+// Peak absolute sample of the most recent capture block, 0.0 to 1.0, so the
+// page can show a level meter and the user can tell the microphones are live.
+float peakLevel();
+
+// Human-readable reason the last operation failed, or "" if all is well.
+const char *lastError();
+
+// The PSRAM recording, for the download handler. Null when empty or when the
+// last recording went somewhere else.
+const uint8_t *psramData();
+uint32_t psramSize();
+
+// Discards the PSRAM recording and frees the buffer.
+void clearPsram();
+
+// Name the current or last recording was given, without a path or extension.
+const char *recordingName();
+
+}  // namespace audio

+ 515 - 0
src/audio_portal.cpp

@@ -0,0 +1,515 @@
+#include "audio_portal.h"
+
+#include <ArduinoJson.h>
+#include <FS.h>
+#include <SD_MMC.h>
+
+#include "audio.h"
+#include "config.h"
+
+namespace {
+
+WebServer *g_server = nullptr;
+
+// One self-contained document, same rule as the settings page: no CDN, no
+// external fonts. The styling deliberately mirrors portal.cpp so the two pages
+// read as one device rather than two projects.
+const char AUDIO_HTML[] PROGMEM = R"HTML(<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<title>Temperature Display Audio</title>
+<style>
+:root{color-scheme:light dark}
+*{box-sizing:border-box}
+body{margin:0;font:16px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
+background:#f4f4f5;color:#18181b}
+main{max-width:34rem;margin:0 auto;padding:24px 16px 48px}
+h1{font-size:1.4rem;margin:0 0 4px}
+p.sub{margin:0 0 24px;color:#71717a;font-size:.9rem}
+fieldset{border:1px solid #d4d4d8;border-radius:10px;background:#fff;margin:0 0 20px;padding:16px}
+legend{padding:0 6px;font-weight:600;font-size:.85rem;text-transform:uppercase;letter-spacing:.04em;color:#52525b}
+label{display:block;margin:12px 0 4px;font-size:.85rem;font-weight:600}
+label:first-of-type{margin-top:0}
+input,select{width:100%;padding:10px;font-size:1rem;border:1px solid #d4d4d8;border-radius:8px;
+background:#fff;color:inherit}
+input:focus,select:focus{outline:2px solid #2563eb;outline-offset:-1px;border-color:#2563eb}
+small{display:block;margin-top:4px;color:#71717a;font-size:.78rem}
+button{width:100%;padding:12px;font-size:1rem;font-weight:600;border:0;border-radius:8px;
+background:#2563eb;color:#fff;cursor:pointer}
+button:disabled{opacity:.5;cursor:default}
+button.alt{background:#e4e4e7;color:#18181b;margin-top:8px}
+button.rec{background:#dc2626}
+button.link{background:none;color:#2563eb;width:auto;padding:4px 0;font-size:.85rem;text-decoration:underline}
+#status{display:flex;gap:8px;margin-bottom:12px}
+#status div{flex:1;padding:8px 6px;border:1px solid #d4d4d8;border-radius:8px;
+background:#fafafa;text-align:center}
+#status b{display:block;font-size:1.1rem}
+#status span{font-size:.7rem;font-weight:600;letter-spacing:.06em;
+text-transform:uppercase;color:#71717a}
+#meter{height:10px;border-radius:5px;background:#e4e4e7;overflow:hidden}
+#meter i{display:block;height:100%;width:0;background:#16a34a;transition:width .1s linear}
+#files{list-style:none;margin:12px 0 0;padding:0}
+#files li{display:flex;align-items:center;gap:8px;padding:8px 0;border-top:1px solid #e4e4e7;font-size:.9rem}
+#files li a{flex:1;color:#2563eb}
+#files button{width:auto;padding:6px 10px;font-size:.8rem;background:#e4e4e7;color:#18181b}
+audio{width:100%;margin-top:12px}
+#msg{margin:16px 0 0;padding:12px;border-radius:8px;display:none;font-size:.9rem}
+#msg.ok{display:block;background:#dcfce7;color:#14532d}
+#msg.err{display:block;background:#fee2e2;color:#7f1d1d}
+#msg.busy{display:block;background:#e0e7ff;color:#1e3a8a}
+.hide{display:none}
+@media(prefers-color-scheme:dark){
+body{background:#18181b;color:#f4f4f5}
+fieldset{background:#27272a;border-color:#3f3f46}
+input,select{background:#18181b;border-color:#3f3f46}
+legend,small,p.sub{color:#a1a1aa}
+#status div{background:#18181b;border-color:#3f3f46}
+#status span{color:#a1a1aa}
+#meter{background:#3f3f46}
+#files li{border-color:#3f3f46}
+#files button,button.alt{background:#3f3f46;color:#f4f4f5}
+#msg.ok{background:#14532d;color:#dcfce7}
+#msg.err{background:#7f1d1d;color:#fee2e2}
+#msg.busy{background:#1e3a8a;color:#e0e7ff}
+}
+</style>
+</head>
+<body>
+<main>
+<h1>Audio</h1>
+<p class="sub">Record from the two onboard microphones as MP3.</p>
+
+<fieldset>
+<legend>Status</legend>
+<div id="status">
+<div><b id="stState">--</b><span>State</span></div>
+<div><b id="stTime">0:00</b><span>Length</span></div>
+<div><b id="stSize">0 KB</b><span>Size</span></div>
+</div>
+<div id="meter"><i id="meterFill"></i></div>
+<small>Input level from MIC1 and MIC2.</small>
+</fieldset>
+
+<fieldset>
+<legend>Record</legend>
+<label for="dest">Destination</label>
+<select id="dest">
+<option value="psram">Memory - download when finished</option>
+<option value="sd">SD card</option>
+<option value="stream">Listen live in this browser</option>
+</select>
+<small id="destHint"></small>
+
+<label for="chan">Channels</label>
+<select id="chan">
+<option value="0">Mono - both microphones mixed</option>
+<option value="1">Stereo - MIC1 left, MIC2 right</option>
+</select>
+
+<label for="name">Name</label>
+<input id="name" placeholder="Leave blank for a timestamp">
+
+<button id="rec">Start recording</button>
+<button id="dl" class="alt hide">Download recording</button>
+<audio id="player" class="hide" controls></audio>
+</fieldset>
+
+<fieldset id="sdBox" class="hide">
+<legend>On the SD card</legend>
+<ul id="files"></ul>
+<small id="noFiles">No recordings yet.</small>
+</fieldset>
+
+<button class="link" onclick="location.href='/'">Back to settings</button>
+<p id="msg"></p>
+</main>
+<script>
+var el = function (id) { return document.getElementById(id); };
+var recording = false;
+var available = true;
+var timer = null;
+
+function msg(text, kind) {
+  var m = el('msg');
+  m.textContent = text;
+  m.className = kind || '';
+}
+
+function fmtTime(ms) {
+  var s = Math.floor(ms / 1000);
+  var m = Math.floor(s / 60);
+  var r = s % 60;
+  return m + ':' + (r < 10 ? '0' : '') + r;
+}
+
+function fmtSize(b) {
+  if (b < 1024) { return b + ' B'; }
+  if (b < 1024 * 1024) { return Math.round(b / 1024) + ' KB'; }
+  return (b / 1048576).toFixed(1) + ' MB';
+}
+
+function destHint() {
+  var d = el('dest').value;
+  var t = '';
+  if (d === 'psram') {
+    t = 'Held in memory and lost on reboot. Stops automatically at about 20 minutes.';
+  } else if (d === 'sd') {
+    t = 'Written straight to the card, limited only by free space.';
+  } else {
+    t = 'Played here as it is encoded. Nothing is kept.';
+  }
+  el('destHint').textContent = t;
+}
+
+function apply(s) {
+  recording = s.state === 'recording';
+  available = s.available;
+
+  el('stState').textContent = available
+    ? (recording ? 'Recording' : (s.state === 'failed' ? 'Failed' : 'Idle'))
+    : 'No codec';
+  el('stTime').textContent = fmtTime(s.elapsedMs);
+  el('stSize').textContent = fmtSize(s.bytes);
+  el('meterFill').style.width = Math.round(Math.min(1, s.peak) * 100) + '%';
+
+  el('rec').textContent = recording ? 'Stop recording' : 'Start recording';
+  el('rec').className = recording ? 'rec' : '';
+  el('rec').disabled = !available;
+  el('dest').disabled = recording;
+  el('chan').disabled = recording;
+  el('name').disabled = recording;
+
+  var canDownload = !recording && s.psramBytes > 0;
+  el('dl').className = canDownload ? 'alt' : 'alt hide';
+
+  if (s.sd) {
+    el('sdBox').className = '';
+  }
+  if (!available) {
+    msg(s.error || 'The microphone codec did not respond.', 'err');
+  } else if (s.state === 'failed' && s.error) {
+    msg(s.error, 'err');
+  }
+}
+
+function poll() {
+  fetch('/api/audio/status').then(function (r) { return r.json(); }).then(function (s) {
+    apply(s);
+    if (!recording && timer) { clearInterval(timer); timer = null; loadFiles(); }
+  }).catch(function () {});
+}
+
+function loadFiles() {
+  fetch('/api/audio/files').then(function (r) { return r.json(); }).then(function (d) {
+    var ul = el('files');
+    ul.innerHTML = '';
+    if (!d.files || !d.files.length) {
+      el('noFiles').className = '';
+      return;
+    }
+    el('noFiles').className = 'hide';
+    d.files.forEach(function (f) {
+      var li = document.createElement('li');
+      var a = document.createElement('a');
+      a.href = '/api/audio/file?name=' + encodeURIComponent(f.name);
+      a.textContent = f.name + ' (' + fmtSize(f.size) + ')';
+      var b = document.createElement('button');
+      b.textContent = 'Delete';
+      b.onclick = function () {
+        fetch('/api/audio/delete?name=' + encodeURIComponent(f.name), { method: 'POST' })
+          .then(loadFiles);
+      };
+      li.appendChild(a);
+      li.appendChild(b);
+      ul.appendChild(li);
+    });
+  }).catch(function () {});
+}
+
+function startRecording() {
+  var dest = el('dest').value;
+  var stereo = el('chan').value;
+  var name = encodeURIComponent(el('name').value);
+
+  if (dest === 'stream') {
+    // The stream endpoint starts the recording itself, because the recording
+    // has nowhere to go until this request has arrived.
+    var p = el('player');
+    p.className = '';
+    p.src = '/api/audio/stream?stereo=' + stereo;
+    p.play().catch(function () {});
+    msg('Streaming. Playback lags a few seconds behind.', 'busy');
+    recording = true;
+    if (!timer) { timer = setInterval(poll, 500); }
+    return;
+  }
+
+  var url = '/api/audio/start?dest=' + dest + '&stereo=' + stereo + '&name=' + name;
+  fetch(url, { method: 'POST' }).then(function (r) { return r.json(); }).then(function (d) {
+    if (!d.ok) { msg(d.error || 'Could not start.', 'err'); return; }
+    msg('Recording.', 'busy');
+    if (!timer) { timer = setInterval(poll, 500); }
+  });
+}
+
+function stopRecording() {
+  el('player').src = '';
+  el('player').className = 'hide';
+  fetch('/api/audio/stop', { method: 'POST' }).then(function () {
+    msg('Stopped.', 'ok');
+    poll();
+  });
+}
+
+el('rec').onclick = function () {
+  if (recording) { stopRecording(); } else { startRecording(); }
+};
+el('dl').onclick = function () { location.href = '/api/audio/download'; };
+el('dest').onchange = destHint;
+
+destHint();
+poll();
+loadFiles();
+</script>
+</body>
+</html>
+)HTML";
+
+const char *destName(audio::Destination d)
+{
+  switch (d) {
+    case audio::Destination::Sd:
+      return "sd";
+    case audio::Destination::Stream:
+      return "stream";
+    default:
+      return "psram";
+  }
+}
+
+void sendJson(int code, const JsonDocument &doc)
+{
+  String out;
+  serializeJson(doc, out);
+  g_server->send(code, "application/json", out);
+}
+
+void handlePage()
+{
+  g_server->sendHeader("Cache-Control", "no-store");
+  g_server->send_P(200, "text/html", AUDIO_HTML);
+}
+
+void handleStatus()
+{
+  JsonDocument doc;
+  const audio::State s = audio::state();
+  doc["state"] = s == audio::State::Recording ? "recording"
+                 : s == audio::State::Failed  ? "failed"
+                                              : "idle";
+  doc["available"] = audio::isAvailable();
+  doc["sd"] = audio::hasSdCard();
+  doc["elapsedMs"] = audio::elapsedMs();
+  doc["bytes"] = audio::encodedBytes();
+  doc["psramBytes"] = audio::psramSize();
+  doc["peak"] = audio::peakLevel();
+  doc["name"] = audio::recordingName();
+  doc["error"] = audio::lastError();
+  sendJson(200, doc);
+}
+
+audio::Destination parseDest(const String &s)
+{
+  if (s == "sd") {
+    return audio::Destination::Sd;
+  }
+  if (s == "stream") {
+    return audio::Destination::Stream;
+  }
+  return audio::Destination::Psram;
+}
+
+void handleStart()
+{
+  const audio::Destination dest = parseDest(g_server->arg("dest"));
+  const bool stereo = g_server->arg("stereo") == "1";
+  const String name = g_server->arg("name");
+
+  JsonDocument doc;
+  if (dest == audio::Destination::Stream) {
+    // Nothing to write to: the browser reaches the stream through /stream.
+    doc["ok"] = false;
+    doc["error"] = "use the stream endpoint for live listening";
+    sendJson(400, doc);
+    return;
+  }
+
+  const bool ok = audio::start(dest, stereo, name.c_str());
+  doc["ok"] = ok;
+  doc["dest"] = destName(dest);
+  if (!ok) {
+    doc["error"] = audio::lastError();
+  }
+  sendJson(ok ? 200 : 409, doc);
+}
+
+void handleStop()
+{
+  audio::stop();
+  JsonDocument doc;
+  doc["ok"] = true;
+  doc["bytes"] = audio::encodedBytes();
+  sendJson(200, doc);
+}
+
+// Hands the live connection to the audio module and starts recording into it.
+// The response has no Content-Length and is closed when the recording stops,
+// which is how a browser plays a progressive MP3 stream.
+void handleStream()
+{
+  const bool stereo = g_server->arg("stereo") == "1";
+
+  if (audio::isRecording()) {
+    g_server->send(409, "text/plain", "already recording");
+    return;
+  }
+
+  WiFiClient client = g_server->client();
+  if (!client.connected()) {
+    return;
+  }
+
+  client.print(F("HTTP/1.1 200 OK\r\n"
+                 "Content-Type: audio/mpeg\r\n"
+                 "Cache-Control: no-store\r\n"
+                 "Connection: close\r\n\r\n"));
+
+  // The WebServer drops its reference to this socket when the handler returns,
+  // but the underlying descriptor is reference counted, so the copy kept here
+  // holds the connection open.
+  audio::attachStreamClient(client);
+
+  if (!audio::start(audio::Destination::Stream, stereo, "")) {
+    client.stop();
+  }
+}
+
+void handleDownload()
+{
+  const uint8_t *data = audio::psramData();
+  const uint32_t len = audio::psramSize();
+  if (data == nullptr || len == 0) {
+    g_server->send(404, "text/plain", "no recording in memory");
+    return;
+  }
+
+  char disp[96];
+  snprintf(disp, sizeof(disp), "attachment; filename=\"%s.mp3\"",
+           audio::recordingName());
+  g_server->sendHeader("Content-Disposition", disp);
+  g_server->setContentLength(len);
+  g_server->send(200, "audio/mpeg", "");
+
+  // Sent in pieces so a multi-megabyte recording never needs a second copy.
+  const uint32_t chunk = 4096;
+  for (uint32_t off = 0; off < len; off += chunk) {
+    const uint32_t n = (len - off) < chunk ? (len - off) : chunk;
+    if (g_server->client().write(data + off, n) != n) {
+      break;  // the browser gave up
+    }
+  }
+}
+
+void handleFiles()
+{
+  JsonDocument doc;
+  JsonArray files = doc["files"].to<JsonArray>();
+
+  if (audio::hasSdCard()) {
+    File root = SD_MMC.open("/");
+    if (root) {
+      for (File f = root.openNextFile(); f; f = root.openNextFile()) {
+        String n = f.name();
+        // openNextFile() returns a leading slash on some core versions.
+        if (n.startsWith("/")) {
+          n = n.substring(1);
+        }
+        if (n.endsWith(".mp3")) {
+          JsonObject o = files.add<JsonObject>();
+          o["name"] = n;
+          o["size"] = (uint32_t)f.size();
+        }
+        f.close();
+      }
+      root.close();
+    }
+  }
+  sendJson(200, doc);
+}
+
+// Rejects anything that is not a plain filename, so a request cannot reach
+// outside the card's root.
+bool safeName(const String &name)
+{
+  if (name.isEmpty() || name.length() > 48) {
+    return false;
+  }
+  if (name.indexOf('/') >= 0 || name.indexOf('\\') >= 0 || name.indexOf("..") >= 0) {
+    return false;
+  }
+  return name.endsWith(".mp3");
+}
+
+void handleFile()
+{
+  const String name = g_server->arg("name");
+  if (!audio::hasSdCard() || !safeName(name)) {
+    g_server->send(404, "text/plain", "not found");
+    return;
+  }
+
+  File f = SD_MMC.open("/" + name, FILE_READ);
+  if (!f) {
+    g_server->send(404, "text/plain", "not found");
+    return;
+  }
+  g_server->sendHeader("Content-Disposition",
+                       "attachment; filename=\"" + name + "\"");
+  g_server->streamFile(f, "audio/mpeg");
+  f.close();
+}
+
+void handleDelete()
+{
+  const String name = g_server->arg("name");
+  JsonDocument doc;
+  if (!audio::hasSdCard() || !safeName(name)) {
+    doc["ok"] = false;
+    sendJson(400, doc);
+    return;
+  }
+  doc["ok"] = SD_MMC.remove("/" + name);
+  sendJson(200, doc);
+}
+
+}  // namespace
+
+namespace audio_portal {
+
+void registerHandlers(WebServer &server)
+{
+  g_server = &server;
+  server.on("/audio", HTTP_GET, handlePage);
+  server.on("/api/audio/status", HTTP_GET, handleStatus);
+  server.on("/api/audio/start", HTTP_POST, handleStart);
+  server.on("/api/audio/stop", HTTP_POST, handleStop);
+  server.on("/api/audio/stream", HTTP_GET, handleStream);
+  server.on("/api/audio/download", HTTP_GET, handleDownload);
+  server.on("/api/audio/files", HTTP_GET, handleFiles);
+  server.on("/api/audio/file", HTTP_GET, handleFile);
+  server.on("/api/audio/delete", HTTP_POST, handleDelete);
+}
+
+}  // namespace audio_portal

+ 18 - 0
src/audio_portal.h

@@ -0,0 +1,18 @@
+#pragma once
+
+#include <WebServer.h>
+
+// The /audio page and its API.
+//
+// Split out of portal.cpp rather than added to it: the settings page and the
+// recorder have nothing to say to each other, and portal.cpp is already one
+// long self-contained document. The same rule still applies here — the page is
+// one file with no CDN and no external fonts, because in setup mode the phone
+// loading it has no route to the internet.
+namespace audio_portal {
+
+// Adds the audio routes to the portal's server. Called from
+// portal's registerHandlers() so both modes get the page.
+void registerHandlers(WebServer &server);
+
+}  // namespace audio_portal

+ 11 - 0
src/main.cpp

@@ -9,6 +9,7 @@
 #include <time.h>
 
 #include "ambient.h"
+#include "audio.h"
 #include "battery.h"
 #include "config.h"
 #include "display.h"
@@ -140,6 +141,12 @@ void setup()
   ambient::begin();
   outdoor::begin();
 
+  // Ahead of the setup-mode branch, because the portal serves /audio in both
+  // modes. Probes the codec and mounts the card; it does not power up the
+  // microphones or install the I2S driver, so it costs little before the first
+  // draw.
+  audio::begin();
+
   // Without stored credentials there is nothing to connect to and no way for
   // the user to supply them except the portal, so it takes over the screen.
   if (forceSetup || !settings::hasWifi()) {
@@ -160,6 +167,10 @@ void loop()
 {
   portal::loop();
 
+  // Drains the encoder to a listening browser and tears a finished recording
+  // down. Bounded work, so the clock below still runs on time.
+  audio::loop();
+
   if (g_setupMode) {
     // The setup screen is static and the clock is almost certainly wrong;
     // serving the portal promptly is all that matters here.

+ 3 - 0
src/portal.cpp

@@ -8,6 +8,7 @@
 #include <WiFi.h>
 
 #include "ambient.h"
+#include "audio_portal.h"
 #include "battery.h"
 #include "config.h"
 #include "display.h"
@@ -187,6 +188,7 @@ button.alt{background:#3f3f46;color:#f4f4f5}
 
 <button type="submit" id="save">Save and restart</button>
 <button type="button" class="alt" id="factory">Erase all settings</button>
+<button type="button" class="link" onclick="location.href='/audio'">Record from the microphones</button>
 <p id="msg"></p>
 </form>
 </main>
@@ -681,6 +683,7 @@ void registerHandlers()
   g_server.on("/api/preview", HTTP_GET, handlePreview);
   g_server.on("/api/status", HTTP_GET, handleStatus);
   g_server.on("/api/reset", HTTP_POST, handleReset);
+  audio_portal::registerHandlers(g_server);
   g_server.onNotFound(handleNotFound);
 }
 

+ 6 - 0
src/power.cpp

@@ -4,6 +4,7 @@
 #include <esp_sleep.h>
 
 #include "ambient.h"
+#include "audio.h"
 #include "config.h"
 #include "display.h"
 #include "network.h"
@@ -28,6 +29,11 @@ bool sleepingWanted()
   if (portal::isCaptive()) {
     return false;
   }
+  // Light sleep would stop the encoder mid-recording and drop Wi-Fi under a
+  // listening browser. A dark room is no reason to abandon a recording.
+  if (audio::isRecording()) {
+    return false;
+  }
   if (portal::millisSinceLastRequest() < PORTAL_GRACE_MS) {
     return false;
   }