Merge branch 'development' into pre-release-850

This commit is contained in:
Theo Arends 2020-09-02 10:49:12 +02:00
commit 3195ce7b14
2166 changed files with 328333 additions and 22210 deletions

View File

@ -8,7 +8,7 @@ about: Create a report to help us improve
This issue template is meant to REPORT Tasmota software PROBLEMS ONLY
Please DO NOT OPEN AN ISSUE:
- If your Tasmota version is not the latest from the development branch, please update your device before submitting your issue. Your problem might already be solved. The latest precompiled binaries of Tasmota can be downloaded from http://thehackbox.org/tasmota/
- If your Tasmota version is not the latest from the development branch, please update your device before submitting your issue. Your problem might already be solved. The latest precompiled binaries of Tasmota can be downloaded from http://ota.tasmota.com/tasmota/
- If you have an issue when flashing was done via Tuya Convert, please address it to Tuya Convert Team
- If your issue is a flashing issue, please address it to the [Tasmota Support Chat](https://discord.gg/Ks2Kzd4)
- If your issue is compilation problem, please address it to the [Tasmota Support Chat](https://discord.gg/Ks2Kzd4)

View File

@ -6,7 +6,7 @@
- [ ] The pull request is done against the latest dev branch
- [ ] Only relevant files were touched
- [ ] Only one feature/fix was added per PR.
- [ ] The code change is tested and works on Tasmota core ESP8266 V.2.7.2.1
- [ ] The code change is tested and works on Tasmota core ESP8266 V.2.7.4.1
- [ ] The code change is tested and works on core ESP32 V.1.12.2
- [ ] I accept the [CLA](https://github.com/arendst/Tasmota/blob/development/CONTRIBUTING.md#contributor-license-agreement-cla).

View File

@ -27,6 +27,7 @@
| USE_EXPRESSION | - | - | - | - | - | - | - |
| SUPPORT_IF_STATEMENT | - | - | - | - | - | - | - |
| USE_HOTPLUG | - | - | - | - | - | - | - |
| USE_PROMETHEUS | - | - | - | - | - | - | - | Enables the `/metrics` endpoint
| | | | | | | | |
| Feature or Sensor | minimal | lite | tasmota | knx | sensors | ir | display | Remarks
| ROTARY_V1 | - | - | x | - | x | - | - |

View File

@ -19,7 +19,7 @@ See [CHANGELOG.md](https://github.com/arendst/Tasmota/blob/development/tasmota/C
## Development
[![Dev Version](https://img.shields.io/badge/development%20version-v8.4.x.x-blue.svg)](https://github.com/arendst/Tasmota)
[![Download Dev](https://img.shields.io/badge/download-development-yellow.svg)](http://thehackbox.org/tasmota/)
[![Download Dev](https://img.shields.io/badge/download-development-yellow.svg)](http://ota.tasmota.com/tasmota/)
[![Tasmota CI](https://github.com/arendst/Tasmota/workflows/Tasmota%20CI/badge.svg)](https://github.com/arendst/Tasmota/actions?query=workflow%3A%22Tasmota+CI%22)
[![Tasmota ESP32 CI](https://github.com/arendst/Tasmota/workflows/Tasmota%20ESP32%20CI/badge.svg)](https://github.com/arendst/Tasmota/actions?query=workflow%3A%22Tasmota+ESP32+CI%22)
[![Build_firmware](https://github.com/arendst/Tasmota/workflows/Build_firmware/badge.svg)](https://github.com/arendst/Tasmota/actions?query=workflow%3ABuild_firmware)

View File

@ -64,7 +64,8 @@ Index | Define | Driver | Device | Address(es) | Description
41 | USE_DHT12 | xsns_58 | DHT12 | 0x5C | Temperature and humidity sensor
42 | USE_DS1624 | xsns_59 | DS1621 | 0x48 - 0x4F | Temperature sensor
42 | USE_DS1624 | xsns_59 | DS1624 | 0x48 - 0x4F | Temperature sensor
43 | USE_AHT1x | xsns_63 | AHT10/15 | 0x38 | Temperature and humidity sensor
43 | USE_AHT1x | xsns_63 | AHT10/15 | 0x38 - 0x39 | Temperature and humidity sensor
43 | USE_AHT2x | xsns_63 | AHT20 | 0x38 | Temperature and humidity sensor
44 | USE_WEMOS_MOTOR_V1 | xdrv_34 | | 0x2D - 0x30 | WEMOS motor shield v1.0.0 (6612FNG)
45 | USE_HDC1080 | xsns_65 | HDC1080 | 0x40 | Temperature and Humidity sensor
46 | USE_IAQ | xsns_66 | IAQ | 0x5a | Air quality sensor

View File

@ -2,82 +2,82 @@
The following hardware modules are supported.
Module | Description
-------------------|-----------------------
01 Sonoff Basic | Sonoff Basic Wifi Smart Switch
02 Sonoff RF | Sonoff RF Wifi Smart Switch with RF (434MHz) receiver
03 Sonoff SV | Sonoff SV Safe Voltage Wifi Smart Switch
04 Sonoff TH | Sonoff TH10/TH16 Wifi Smart Switch with Sensor connection
05 Sonoff Dual | Sonoff Dual Wifi Smart Switch
06 Sonoff Pow | Sonoff Pow Wifi Smart Switch with Energy Monitoring
07 Sonoff 4CH | Sonoff 4CH 4-gang Wifi Smart Switch
08 Sonoff S2X | Sonoff S20/S26 Wifi Smart Socket
09 Slampher | Sonoff Slampher Wifi Smart Light Bulb Socket with RF (434MHz) receiver
10 Sonoff Touch | Sonoff Touch Wifi Light Switch
11 Sonoff LED | Sonoff Led Wifi Led Pack (Retired)
12 1 Channel | 1 Channel Inching/Self Locking Wifi Switch 5V/12V
13 4 Channel | 4 Channel Inching/Self Locking Wifi Switch (Retired)
14 Motor C/AC | Motor Clockwise/Antoclockwise Wifi Switch (Retired)
15 ElectroDragon | Electrodragon Wifi IoT Board
16 EXS Relay(s) | Electronic Experience Store 1 or 2-gang Wifi Module
17 WiOn | WiOn Wifi Smart Socket
18 Generic | Any ESP8266/ESP8285 device like WeMos and NodeMCU
19 Sonoff Dev | Sonoff Dev Wifi Development Board
20 H801 | H801 Wifi 5 Channel LED Controller
21 Sonoff SC | Sonoff SC Wifi Environmental Monitor
22 Sonoff BN-SZ | Sonoff BN-SZ01 Wifi Ceiling Led (Retired)
23 Sonoff 4CH Pro | Sonoff 4CH Pro 4-gang Wifi Smart Switch
24 Huafan SS | HuaFan Wifi Smart Socket
25 Sonoff Bridge | Sonoff RF (434MHz) transceive to Wifi Bridge
26 Sonoff B1 | Sonoff B1 Wifi RGBWW Led Bulb
27 AiLight | Ai-Thinker RGBW Led Bulb
28 Sonoff T1 1CH | Sonoff T1 1-gang Wifi Light Switch
29 Sonoff T1 2CH | Sonoff T1 2-gang Wifi Light Switch
30 Sonoff T1 3CH | Sonoff T1 3-gang Wifi Light Switch
31 Supla Espablo | 2-gang Wifi Module
32 Witty Cloud | Witty Cloud ESP8266 Wifi Development Board
33 Yunshan Relay | ESP8266 Wifi Network Relay Module
34 MagicHome | MagicHome, Flux-light and some Arilux LC10 RGB(W) Led Controller
35 Luani HVIO | Luani ESP8266 Wifi I/O Module
36 KMC 70011 | KMC Wifi Smart Socket with Energy Monitoring
37 Arilux LC01 | Arilux AL-LC01 RGB Led Controller
38 Arilux LC11 | Arilux AL-LC11 RGBWW Led Controller
39 Sonoff Dual R2 | Sonoff Dual R2 Wifi Smart Switch
40 Arilux LC06 | Arilux AL-LC06 RGB(WW) Led Controller
41 Sonoff S31 | Sonoff S31 Wifi Smart Socket with Energy Monitoring
42 Zengge WF017 | Zengge WF017 Wifi RGB(W) Led Controller
43 Sonoff Pow R2 | Sonoff Pow R2 Wifi Smart Switch with Energy Monitoring
44 Sonoff iFan02 | Sonoff iFan02 Wifi Smart Ceiling Fan with Light
45 BlitzWolf SHP | BlitzWolf BW-SHP2, BW-SHP6, HomeCube SP1, Gosund SP111, Teckin SP22 Wifi Smart Switch with Energy Monitoring
46 Shelly 1 | Shelly 1 Open Source Wifi Relay Module
47 Shelly 2 | Shelly 2 Wifi 2-gang Relay Module with Energy Monitoring
48 Xiaomi Philips | Xiaomi Philips Wifi WW Led Bulb
49 Neo Coolcam | Neo Coolcam Wifi Smart Socket
50 ESP Switch | ESP Switch 4-gang Wifi Switch with Leds
51 OBI Socket | OBI Wifi Smart Socket
52 Teckin | Teckin SP22 Wifi Smart Switch with Energy Monitoring
53 AplicWDP303075 | Aplic WDP 303075 CSL Wifi Smart Switch with Energy Monitoring
54 TuyaMCU | Devices with an MCU using Tuya communication protocol for control
55 Gosund SP1 v23 | Gosund SP1 v2.3 Wifi Smart Switch with Energy Monitoring
56 ARMTR Dimmer | ARMtronix Wifi dimmer for Incandescent Lights and Led
57 SK03 Outdoor | SK03 Outdoor Wifi Smart Switch with Energy Monitoring
58 PS-16-DZ | PS-16-DZ Wifi dimmer for Incandescent Lights and Led
59 Teckin US | Teckin SP20 and ZooZee SA102 Wifi Smart Switch with Energy Monitoring
60 Manzoku strip | Manzoku Wifi Smart Power Strip with four Relays
61 OBI Socket 2 | OBI 2 Wifi Smart Socket
62 YTF IR Bridge | YTF Universal IR Bridge
63 Digoo DG-SP202 | Digoo DG-SP202 Dual Wifi Smart Switch with Energy Monitoring
64 KA10 | Smanergy KA10 Wifi Smart Wall Switch with Energy Monitoring
65 Luminea ZX2820 | Luminea ZX2820 Wifi Smart Switch with Energy Monitoring
66 Mi Desk Lamp | Mi Desk Lamp with rotary switch and Wifi
67 SP10 | Tuya SP10 Wifi Smart Switch with Energy Monitoring
68 WAGA CHCZ02MB | WAGA life CHCZ02MB Wifi Smart Switch with Energy Monitoring
69 SYF05 | Sunyesmart SYF05 RGBWW Wifi Led Bulb
70 Sonoff L1 | Sonoff L1 light strip
71 Sonoff iFan03 | Sonoff iFan03 Wifi Smart Ceiling Fan with Light
72 EXS Dimmer | EXS Wifi Dimmer v4
73 PWM Dimmer | Martin Jerry/acenx/Tessan/NTONPOWER SD0x PWM Dimmer Switches
74 Sonoff D1 | Sonoff D1 Wifi and RF Dimmer
75 Sonoff ZbBridge | Sonoff Zigbee bridge
Module | LCode | Description
-------------------|-------|-----------------------
01 Sonoff Basic | | Sonoff Basic Wifi Smart Switch
02 Sonoff RF | | Sonoff RF Wifi Smart Switch with RF (434MHz) receiver
03 Sonoff SV | | Sonoff SV Safe Voltage Wifi Smart Switch
04 Sonoff TH | | Sonoff TH10/TH16 Wifi Smart Switch with Sensor connection
05 Sonoff Dual | x | Sonoff Dual Wifi Smart Switch
06 Sonoff Pow | | Sonoff Pow Wifi Smart Switch with Energy Monitoring
07 Sonoff 4CH | | Sonoff 4CH 4-gang Wifi Smart Switch
08 Sonoff S2X | | Sonoff S20/S26 Wifi Smart Socket
09 Slampher | | Sonoff Slampher Wifi Smart Light Bulb Socket with RF (434MHz) receiver
10 Sonoff Touch | x | Sonoff Touch Wifi Light Switch
11 Sonoff LED | x | Sonoff Led Wifi Led Pack (Retired)
12 1 Channel | | 1 Channel Inching/Self Locking Wifi Switch 5V/12V
13 4 Channel | x | 4 Channel Inching/Self Locking Wifi Switch (Retired)
14 Motor C/AC | x | Motor Clockwise/Antoclockwise Wifi Switch (Retired)
15 ElectroDragon | | Electrodragon Wifi IoT Board
16 EXS Relay(s) | x | Electronic Experience Store 1 or 2-gang Wifi Module
17 WiOn | | WiOn Wifi Smart Socket
18 Generic | x | Any ESP8266/ESP8285 device like WeMos and NodeMCU
19 Sonoff Dev | | Sonoff Dev Wifi Development Board
20 H801 | x | H801 Wifi 5 Channel LED Controller
21 Sonoff SC | x | Sonoff SC Wifi Environmental Monitor
22 Sonoff BN-SZ | x | Sonoff BN-SZ01 Wifi Ceiling Led (Retired)
23 Sonoff 4CH Pro | x | Sonoff 4CH Pro 4-gang Wifi Smart Switch
24 Huafan SS | | HuaFan Wifi Smart Socket
25 Sonoff Bridge | x | Sonoff RF (434MHz) transceive to Wifi Bridge
26 Sonoff B1 | x | Sonoff B1 Wifi RGBWW Led Bulb
27 AiLight | x | Ai-Thinker RGBW Led Bulb
28 Sonoff T1 1CH | x | Sonoff T1 1-gang Wifi Light Switch
29 Sonoff T1 2CH | x | Sonoff T1 2-gang Wifi Light Switch
30 Sonoff T1 3CH | x | Sonoff T1 3-gang Wifi Light Switch
31 Supla Espablo | | 2-gang Wifi Module
32 Witty Cloud | | Witty Cloud ESP8266 Wifi Development Board
33 Yunshan Relay | | ESP8266 Wifi Network Relay Module
34 MagicHome | | MagicHome, Flux-light and some Arilux LC10 RGB(W) Led Controller
35 Luani HVIO | | Luani ESP8266 Wifi I/O Module
36 KMC 70011 | | KMC Wifi Smart Socket with Energy Monitoring
37 Arilux LC01 | | Arilux AL-LC01 RGB Led Controller
38 Arilux LC11 | | Arilux AL-LC11 RGBWW Led Controller
39 Sonoff Dual R2 | x | Sonoff Dual R2 Wifi Smart Switch
40 Arilux LC06 | | Arilux AL-LC06 RGB(WW) Led Controller
41 Sonoff S31 | | Sonoff S31 Wifi Smart Socket with Energy Monitoring
42 Zengge WF017 | | Zengge WF017 Wifi RGB(W) Led Controller
43 Sonoff Pow R2 | | Sonoff Pow R2 Wifi Smart Switch with Energy Monitoring
44 Sonoff iFan02 | x | Sonoff iFan02 Wifi Smart Ceiling Fan with Light
45 BlitzWolf SHP | | BlitzWolf BW-SHP2, BW-SHP6, HomeCube SP1, Gosund SP111, Teckin SP22 Wifi Smart Switch with Energy Monitoring
46 Shelly 1 | | Shelly 1 Open Source Wifi Relay Module
47 Shelly 2 | | Shelly 2 Wifi 2-gang Relay Module with Energy Monitoring
48 Xiaomi Philips | x | Xiaomi Philips Wifi WW Led Bulb
49 Neo Coolcam | | Neo Coolcam Wifi Smart Socket
50 ESP Switch | | ESP Switch 4-gang Wifi Switch with Leds
51 OBI Socket | | OBI Wifi Smart Socket
52 Teckin | | Teckin SP22 Wifi Smart Switch with Energy Monitoring
53 AplicWDP303075 | | Aplic WDP 303075 CSL Wifi Smart Switch with Energy Monitoring
54 TuyaMCU | x | Devices with an MCU using Tuya communication protocol for control
55 Gosund SP1 v23 | | Gosund SP1 v2.3 Wifi Smart Switch with Energy Monitoring
56 ARMTR Dimmer | x | ARMtronix Wifi dimmer for Incandescent Lights and Led
57 SK03 Outdoor | x | SK03 Outdoor Wifi Smart Switch with Energy Monitoring
58 PS-16-DZ | x | PS-16-DZ Wifi dimmer for Incandescent Lights and Led
59 Teckin US | | Teckin SP20 and ZooZee SA102 Wifi Smart Switch with Energy Monitoring
60 Manzoku strip | | Manzoku Wifi Smart Power Strip with four Relays
61 OBI Socket 2 | | OBI 2 Wifi Smart Socket
62 YTF IR Bridge | x | YTF Universal IR Bridge
63 Digoo DG-SP202 | | Digoo DG-SP202 Dual Wifi Smart Switch with Energy Monitoring
64 KA10 | | Smanergy KA10 Wifi Smart Wall Switch with Energy Monitoring
65 Luminea ZX2820 | | Luminea ZX2820 Wifi Smart Switch with Energy Monitoring
66 Mi Desk Lamp | | Mi Desk Lamp with rotary switch and Wifi
67 SP10 | | Tuya SP10 Wifi Smart Switch with Energy Monitoring
68 WAGA CHCZ02MB | | WAGA life CHCZ02MB Wifi Smart Switch with Energy Monitoring
69 SYF05 | | Sunyesmart SYF05 RGBWW Wifi Led Bulb
70 Sonoff L1 | x | Sonoff L1 light strip
71 Sonoff iFan03 | x | Sonoff iFan03 Wifi Smart Ceiling Fan with Light
72 EXS Dimmer | x | EXS Wifi Dimmer v4
73 PWM Dimmer | x | Martin Jerry/acenx/Tessan/NTONPOWER SD0x PWM Dimmer Switches
74 Sonoff D1 | x | Sonoff D1 Wifi and RF Dimmer
75 Sonoff ZbBridge | x | Sonoff Zigbee bridge
Over 1400 additional devices are supported using [templates](TEMPLATES.md).

View File

@ -18,12 +18,12 @@ If you like **Tasmota**, give it a star, or fork it and contribute!
See [RELEASENOTES.md](RELEASENOTES.md) for release information.
In addition to the [release webpage](https://github.com/arendst/Tasmota/releases/latest) the binaries can also be downloaded from http://thehackbox.org/tasmota/release/
In addition to the [release webpage](https://github.com/arendst/Tasmota/releases/latest) the binaries can also be downloaded from http://ota.tasmota.com/tasmota/release/
## Development
[![Dev Version](https://img.shields.io/badge/development%20version-v8.4.x.x-blue.svg)](https://github.com/arendst/Tasmota)
[![Download Dev](https://img.shields.io/badge/download-development-yellow.svg)](http://thehackbox.org/tasmota/)
[![Download Dev](https://img.shields.io/badge/download-development-yellow.svg)](http://ota.tasmota.com/tasmota/)
[![Tasmota CI](https://github.com/arendst/Tasmota/workflows/Tasmota%20CI/badge.svg)](https://github.com/arendst/Tasmota/actions?query=workflow%3A%22Tasmota+CI%22)
[![Tasmota ESP32 CI](https://github.com/arendst/Tasmota/workflows/Tasmota%20ESP32%20CI/badge.svg)](https://github.com/arendst/Tasmota/actions?query=workflow%3A%22Tasmota+ESP32+CI%22)
[![Build_firmware](https://github.com/arendst/Tasmota/workflows/Build_firmware/badge.svg)](https://github.com/arendst/Tasmota/actions?query=workflow%3ABuild_firmware)
@ -32,7 +32,7 @@ See [tasmota/CHANGELOG.md](tasmota/CHANGELOG.md) for detailed change information
Unless your Tasmota powered device exhibits a problem or you need to make use of a feature that is not available in the Tasmota version currently installed on your device, leave your device alone - it works so don't make unnecessary changes! If the release version (i.e., the master branch) exhibits unexpected behaviour for your device and configuration, you should upgrade to the latest development version instead to see if your problem is resolved as some bugs in previous releases or development builds may already have been resolved.
The Tasmota development codebase is checked every 1-2 hours for changes. If new commits have been merged and they compile successfuly, new binary files for every variant will be posted at http://thehackbox.org/tasmota/ (this web address can be used for OTA updates too). The last compiled commit number is also indicated on the same page. It is important to note that these binaries are based on the current development codebase. These commits are tested as much as is possible and are typically quite stable. However, it is infeasible to test on the hundreds of different types of devices with all the available configuration options permitted.
The Tasmota development codebase is checked every 1-2 hours for changes. If new commits have been merged and they compile successfuly, new binary files for every variant will be posted at http://ota.tasmota.com/tasmota/ (this web address can be used for OTA updates too). It is important to note that these binaries are based on the current development codebase. These commits are tested as much as is possible and are typically quite stable. However, it is infeasible to test on the hundreds of different types of devices with all the available configuration options permitted.
Note that there is a chance, as with any upgrade, that the device may not function as expected. You must always account for the possibility that you may need to flash the device via the serial programming interface if the OTA upgrade fails. Even with the master release, you should always attempt to test the device or a similar prototype before upgrading a device which is in production or is hard to reach. And, as always, make a backup of the device configuration before beginning any firmware update.

View File

@ -53,57 +53,21 @@ The following binary downloads have been compiled with ESP8266/Arduino library c
## Changelog
### Version 8.4.0 George
### Version 8.4.0.3
- Remove Arduino ESP8266 Core support for versions before 2.7.1
- Change to limited support of Arduino IDE as an increasing amount of features cannot be compiled with Arduino IDE
- Change IRRemoteESP8266 library from v2.7.6 to v2.7.8.10, fixing Samsung and Pioneer protocols (#8938)
- Change Adafruit_SGP30 library from v1.0.3 to v1.2.0 (#8519)
- Change Energy JSON Total field from ``"Total":[33.736,11.717,16.978]`` to ``"Total":33.736,"TotalTariff":[11.717,16.978]``
- Change Energy JSON ExportActive field from ``"ExportActive":[33.736,11.717,16.978]`` to ``"ExportActive":33.736,"ExportTariff":[11.717,16.978]``
- Change ESP32 USER GPIO template representation decreasing template message size
- Change define USE_TASMOTA_SLAVE into USE_TASMOTA_CLIENT
- Change commands ``SlaveSend`` and ``SlaveReset`` into ``ClientSend`` and ``ClientReset``
- Change all timer references from ``Arm`` to ``Enable`` in GUI, ``Timer`` command and JSON message
- Change Domoticz commands prefix from ``Domoticz`` to ``Dz``
- Change Zigbee randomizing of parameters at first run or after Reset
- Fix escape of non-JSON received serial data (#8329)
- Fix exception or watchdog on rule re-entry (#8757)
- Add command ``Rule0`` to change global rule parameters
- Add command ``Time 4`` to display timestamp using milliseconds (#8537)
- Add command ``SetOption94 0/1`` to select MAX31855 or MAX6675 thermocouple support (#8616)
- Add command ``SetOption97 0/1`` to switch between Tuya serial speeds 9600 bps (0) or 115200 bps (1)
- Add command ``SetOption98 0/1`` to provide rotary rule triggers (1) instead of controlling light (0)
- Add command ``SetOption99 0/1`` to enable zero cross detection on PWM dimmer
- Add command ``SetOption100 0/1`` to remove Zigbee ``ZbReceived`` value from ``{"ZbReceived":{xxx:yyy}}`` JSON message
- Add command ``SetOption101 0/1`` to add the Zigbee source endpoint as suffix to attributes, ex `Power3` instead of `Power` if sent from endpoint 3
- Add command ``DzSend<type> <index>,<value1(;value2)|state>`` to send values or state to Domoticz
- Add command ``Module2`` to configure fallback module on fast reboot (#8464)
- Add command (``S``)``SerialSend6`` \<comma seperated values\> (#8937)
- Add commands ``LedPwmOn 0..255``, ``LedPwmOff 0..255`` and ``LedPwmMode1 0/1`` to control led brightness by George (#8491)
- Add ESP32 ethernet commands ``EthType 0/1``, ``EthAddress 0..31`` and ``EthClockMode 0..3``
- Add more functionality to command ``Switchmode`` 11 and 12 (#8450)
- Add rule trigger ``System#Init`` to allow early rule execution without wifi and mqtt initialized yet
- Add support for unique MQTTClient (and inherited fallback topic) by full Mac address using ``mqttclient DVES_%12X`` (#8300)
- Add wildcard pattern ``?`` for JSON matching in rules
- Add Three Phase Export Active Energy to SDM630 driver
- Add Zigbee options to ``ZbSend`` to write and report attributes
- Add Zigbee auto-responder for common attributes
- Add ``CpuFrequency`` to ``status 2``
- Add ``FlashFrequency`` to ``status 4``
- Add compile time interlock parameters (#8759)
- Add compile time user template (#8766)
- Add support for VEML6075 UVA/UVB/UVINDEX Sensor by device111 (#8432)
- Add support for VEML7700 Ambient light intensity Sensor by device111 (#8432)
- Add support for up to two BH1750 sensors controlled by commands ``BH1750Resolution`` and ``BH1750MTime`` (#8139)
- Add support for up to eight MCP9808 temperature sensors by device111 (#8594)
- Add support for BL0940 energy monitor as used in Blitzwolf BW-SHP10 (#8175)
- Add support for Telegram bot (#8619)
- Add support for HP303B Temperature and Pressure sensor by Robert Jaakke (#8638)
- Add support for Energy sensor (Denky) for French Smart Metering meter provided by global Energy Providers, need a adaptater. See dedicated full [blog](http://hallard.me/category/tinfo/) about French teleinformation stuff
- Add support for ESP32 ethernet adding commands ``Wifi 0/1`` and ``Ethernet 0/1`` both default ON
- Add support for single wire LMT01 temperature Sensor by justifiably (#8713)
- Add support for rotary encoder as light dimmer and optional color temperature if button1 still pressed (#8670)
- Add support for switches/relays using an AC detection circuitry e.g. MOES MS-104B or BlitzWolf SS5 (#8606)
- Add support for Schneider Electric iEM3000 series Modbus energy meter by Marius Bezuidenhout
- Add support for Sonoff Zigbee Bridge as module 75 (#8583)
- Remove support for 1-step upgrade from versions before 6.6.0.11 to versions after 8.4.0.1
- Change references from http://thehackbox.org to http://ota.tasmota.com
- Change White blend mode moved to using ``SetOption 105`` instead of ``RGBWWTable``
- Fix ESP32 PWM range
- Fix display power control (#9114)
- Add command ``SetOption108 0/1`` to enable Teleinfo telemetry into Tasmota Energy MQTT (0) or Teleinfo only (1) - Add Zigbee better support for IKEA Motion Sensor
- Add command ``SetOption109 1`` to force gen1 Alexa mode, for Echo Dot 2nd gen devices only
- Add command ``Restart 2`` to halt system. Needs hardware reset or power cycle to restart (#9046)
- Add ESP32 Analog input support for GPIO32 to GPIO39
- Add Zigbee options to ``ZbSend`` ``Config`` and ``ReadCondig``
- Add Zigbee web gui widget for Temp/Humidity/Pressure sensors
- Add Zigbee web ui for power metering plugs
- Add better config corruption recovery (#9046)
- Add virtual CT for 4 channels lights, emulating a 5th channel
- Add support for DYP ME007 ultrasonic distance sensor by Janusz Kostorz (#9113)
- Add command ``PowerDelta1`` to ``PowerDelta3`` to trigger on up to three phases (#9134)

View File

@ -0,0 +1,6 @@
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 0x10000, 0x180000,
app1, app, ota_1, 0x190000, 0x180000,
ffat, data, fat, 0x310000,0x0F0000,
1 # Name Type SubType Offset Size Flags
2 nvs data nvs 0x9000 0x5000
3 otadata data ota 0xe000 0x2000
4 app0 app ota_0 0x10000 0x180000
5 app1 app ota_1 0x190000 0x180000
6 ffat data fat 0x310000 0x0F0000

View File

@ -0,0 +1,6 @@
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 0x10000, 0x1F0000,
app1, app, ota_1, 0x200000, 0x1F0000,
ffat, data, fat, 0x3F0000,0xC10000,
1 # Name Type SubType Offset Size Flags
2 nvs data nvs 0x9000 0x5000
3 otadata data ota 0xe000 0x2000
4 app0 app ota_0 0x10000 0x1F0000
5 app1 app ota_1 0x200000 0x1F0000
6 ffat data fat 0x3F0000 0xC10000

View File

@ -0,0 +1,602 @@
/***************************************************
This is a library for the ST7789 IPS SPI display.
Originally written by Limor Fried/Ladyada for
Adafruit Industries.
Modified by Ananev Ilia
****************************************************/
#include "Arduino_ST7789.h"
#include <limits.h>
#include "pins_arduino.h"
#include "wiring_private.h"
#include <SPI.h>
const uint16_t ST7789_colors[]={ST7789_BLACK,ST7789_WHITE,ST7789_RED,ST7789_GREEN,ST7789_BLUE,ST7789_CYAN,ST7789_MAGENTA,\
ST7789_YELLOW,ST7789_NAVY,ST7789_DARKGREEN,ST7789_DARKCYAN,ST7789_MAROON,ST7789_PURPLE,ST7789_OLIVE,\
ST7789_LIGHTGREY,ST7789_DARKGREY,ST7789_ORANGE,ST7789_GREENYELLOW,ST7789_PINK};
#ifdef ESP32
#define ST7789_DIMMER
#endif
uint16_t Arduino_ST7789::GetColorFromIndex(uint8_t index) {
if (index>=sizeof(ST7789_colors)/2) index=0;
return ST7789_colors[index];
}
static const uint8_t PROGMEM
cmd_240x240[] = { // Initialization commands for 7789 screens
10, // 9 commands in list:
ST7789_SWRESET, ST_CMD_DELAY, // 1: Software reset, no args, w/delay
150, // 150 ms delay
ST7789_SLPOUT , ST_CMD_DELAY, // 2: Out of sleep mode, no args, w/delay
255, // 255 = 500 ms delay
ST7789_COLMOD , 1+ST_CMD_DELAY, // 3: Set color mode, 1 arg + delay:
0x55, // 16-bit color
10, // 10 ms delay
ST7789_MADCTL , 1, // 4: Memory access ctrl (directions), 1 arg:
0x00, // Row addr/col addr, bottom to top refresh
ST7789_CASET , 4, // 5: Column addr set, 4 args, no delay:
0x00, ST7789_240x240_XSTART, // XSTART = 0
(ST7789_TFTWIDTH+ST7789_240x240_XSTART) >> 8,
(ST7789_TFTWIDTH+ST7789_240x240_XSTART) & 0xFF, // XEND = 240
ST7789_RASET , 4, // 6: Row addr set, 4 args, no delay:
0x00, ST7789_240x240_YSTART, // YSTART = 0
(ST7789_TFTHEIGHT+ST7789_240x240_YSTART) >> 8,
(ST7789_TFTHEIGHT+ST7789_240x240_YSTART) & 0xFF, // YEND = 240
ST7789_INVON , ST_CMD_DELAY, // 7: Inversion ON
10,
ST7789_NORON , ST_CMD_DELAY, // 8: Normal display on, no args, w/delay
10, // 10 ms delay
ST7789_DISPON , ST_CMD_DELAY, // 9: Main screen turn on, no args, w/delay
255 }; // 255 = 500 ms delay
inline uint16_t swapcolor(uint16_t x) {
return (x << 11) | (x & 0x07E0) | (x >> 11);
}
#if defined (SPI_HAS_TRANSACTION)
static SPISettings mySPISettings;
#elif defined (__AVR__) || defined(CORE_TEENSY)
static uint8_t SPCRbackup;
static uint8_t mySPCR;
#endif
#if defined (SPI_HAS_TRANSACTION)
#define SPI_BEGIN_TRANSACTION() if (_hwSPI) SPI.beginTransaction(mySPISettings)
#define SPI_END_TRANSACTION() if (_hwSPI) SPI.endTransaction()
#else
#define SPI_BEGIN_TRANSACTION() (void)
#define SPI_END_TRANSACTION() (void)
#endif
// Constructor when using software SPI. All output pins are configurable.
Arduino_ST7789::Arduino_ST7789(int8_t dc, int8_t rst, int8_t sid, int8_t sclk, int8_t cs, int8_t bp)
: Renderer(ST7789_TFTWIDTH, ST7789_TFTHEIGHT)
{
_cs = cs;
_dc = dc;
_sid = sid;
_sclk = sclk;
_rst = rst;
_hwSPI = false;
_bp = bp;
if(dc == -1) _SPI9bit = true;
else _SPI9bit = false;
}
// Constructor when using hardware SPI. Faster, but must use SPI pins
// specific to each board type (e.g. 11,13 for Uno, 51,52 for Mega, etc.)
Arduino_ST7789::Arduino_ST7789(int8_t dc, int8_t rst, int8_t cs, int8_t bp)
: Renderer(ST7789_TFTWIDTH, ST7789_TFTHEIGHT) {
_cs = cs;
_dc = dc;
_rst = rst;
_hwSPI = true;
_SPI9bit = false;
_sid = _sclk = -1;
_bp = bp;
}
void Arduino_ST7789::DisplayInit(int8_t p,int8_t size,int8_t rot,int8_t font) {
setRotation(rot);
//invertDisplay(false);
invertDisplay(true);
//setTextWrap(false); // Allow text to run off edges
//cp437(true);
setTextFont(font&3);
setTextSize(size&7);
setTextColor(ST7789_WHITE,ST7789_BLACK);
setCursor(0,0);
fillScreen(ST7789_BLACK);
}
inline void Arduino_ST7789::spiwrite(uint8_t c)
{
//Serial.println(c, HEX);
if (_hwSPI)
{
#if defined (SPI_HAS_TRANSACTION)
SPI.transfer(c);
#elif defined (__AVR__) || defined(CORE_TEENSY)
SPCRbackup = SPCR;
SPCR = mySPCR;
SPI.transfer(c);
SPCR = SPCRbackup;
#elif defined (__arm__)
SPI.setClockDivider(21); //4MHz
SPI.setDataMode(SPI_MODE2);
SPI.transfer(c);
#endif
}
else
{
if(_SPI9bit)
{
//9s bit send first
#if defined(USE_FAST_IO)
*clkport &= ~clkpinmask;
if(_DCbit) *dataport |= datapinmask;
else *dataport &= ~datapinmask;
*clkport |= clkpinmask;
#else
digitalWrite(_sclk, LOW);
if(_DCbit) digitalWrite(_sid, HIGH);
else digitalWrite(_sid, LOW);
digitalWrite(_sclk, HIGH);
#endif
// Fast SPI bitbang swiped from LPD8806 library
for(uint8_t bit = 0x80; bit; bit >>= 1) {
#if defined(USE_FAST_IO)
*clkport &= ~clkpinmask;
if(c & bit) *dataport |= datapinmask;
else *dataport &= ~datapinmask;
*clkport |= clkpinmask;
#else
digitalWrite(_sclk, LOW);
if(c & bit) digitalWrite(_sid, HIGH);
else digitalWrite(_sid, LOW);
digitalWrite(_sclk, HIGH);
#endif
}
}
else
{
// Fast SPI bitbang swiped from LPD8806 library
for(uint8_t bit = 0x80; bit; bit >>= 1) {
#if defined(USE_FAST_IO)
*clkport &= ~clkpinmask;
if(c & bit) *dataport |= datapinmask;
else *dataport &= ~datapinmask;
*clkport |= clkpinmask;
#else
digitalWrite(_sclk, LOW);
if(c & bit) digitalWrite(_sid, HIGH);
else digitalWrite(_sid, LOW);
digitalWrite(_sclk, HIGH);
#endif
}
}
}
}
void Arduino_ST7789::writecommand(uint8_t c) {
DC_LOW();
CS_LOW();
SPI_BEGIN_TRANSACTION();
spiwrite(c);
CS_HIGH();
SPI_END_TRANSACTION();
}
void Arduino_ST7789::writedata(uint8_t c) {
SPI_BEGIN_TRANSACTION();
DC_HIGH();
CS_LOW();
spiwrite(c);
CS_HIGH();
SPI_END_TRANSACTION();
}
// Companion code to the above tables. Reads and issues
// a series of LCD commands stored in PROGMEM byte array.
void Arduino_ST7789::displayInit(const uint8_t *addr) {
uint8_t numCommands, numArgs;
uint16_t ms;
//<-----------------------------------------------------------------------------------------
DC_HIGH();
#if defined(USE_FAST_IO)
*clkport |= clkpinmask;
#else
digitalWrite(_sclk, HIGH);
#endif
//<-----------------------------------------------------------------------------------------
numCommands = pgm_read_byte(addr++); // Number of commands to follow
while(numCommands--) { // For each command...
writecommand(pgm_read_byte(addr++)); // Read, issue command
numArgs = pgm_read_byte(addr++); // Number of args to follow
ms = numArgs & ST_CMD_DELAY; // If hibit set, delay follows args
numArgs &= ~ST_CMD_DELAY; // Mask out delay bit
while(numArgs--) { // For each argument...
writedata(pgm_read_byte(addr++)); // Read, issue argument
}
if(ms) {
ms = pgm_read_byte(addr++); // Read post-command delay time (ms)
if(ms == 255) ms = 500; // If 255, delay for 500 ms
delay(ms);
}
}
}
// Initialization code common to all ST7789 displays
void Arduino_ST7789::commonInit(const uint8_t *cmdList) {
_ystart = _xstart = 0;
_colstart = _rowstart = 0; // May be overridden in init func
pinMode(_dc, OUTPUT);
if (_cs>=0) {
pinMode(_cs, OUTPUT);
}
if (_bp>=0) {
#define ESP32_PWM_CHANNEL 1
#ifdef ST7789_DIMMER
ledcSetup(ESP32_PWM_CHANNEL,4000,8);
ledcAttachPin(_bp,ESP32_PWM_CHANNEL);
ledcWrite(ESP32_PWM_CHANNEL,128);
#else
pinMode(_bp, OUTPUT);
#endif
}
#if defined(USE_FAST_IO)
dcport = portOutputRegister(digitalPinToPort(_dc));
dcpinmask = digitalPinToBitMask(_dc);
if (_cs>=0) {
csport = portOutputRegister(digitalPinToPort(_cs));
cspinmask = digitalPinToBitMask(_cs);
}
#endif
if(_hwSPI) { // Using hardware SPI
#if defined (SPI_HAS_TRANSACTION)
SPI.begin();
mySPISettings = SPISettings(24000000, MSBFIRST, SPI_MODE2);
#elif defined (__AVR__) || defined(CORE_TEENSY)
SPCRbackup = SPCR;
SPI.begin();
SPI.setClockDivider(SPI_CLOCK_DIV4);
SPI.setDataMode(SPI_MODE2);
mySPCR = SPCR; // save our preferred state
SPCR = SPCRbackup; // then restore
#elif defined (__SAM3X8E__)
SPI.begin();
SPI.setClockDivider(21); //4MHz
SPI.setDataMode(SPI_MODE2);
#endif
} else {
pinMode(_sclk, OUTPUT);
pinMode(_sid , OUTPUT);
digitalWrite(_sclk, LOW);
digitalWrite(_sid, LOW);
#if defined(USE_FAST_IO)
clkport = portOutputRegister(digitalPinToPort(_sclk));
dataport = portOutputRegister(digitalPinToPort(_sid));
clkpinmask = digitalPinToBitMask(_sclk);
datapinmask = digitalPinToBitMask(_sid);
#endif
}
// toggle RST low to reset; CS low so it'll listen to us
CS_LOW();
if (_rst != -1) {
pinMode(_rst, OUTPUT);
digitalWrite(_rst, HIGH);
delay(50);
digitalWrite(_rst, LOW);
delay(50);
digitalWrite(_rst, HIGH);
delay(50);
}
if(cmdList)
displayInit(cmdList);
}
void Arduino_ST7789::setRotation(uint8_t m) {
writecommand(ST7789_MADCTL);
rotation = m % 4; // can't be higher than 3
switch (rotation) {
case 0:
writedata(ST7789_MADCTL_MX | ST7789_MADCTL_MY | ST7789_MADCTL_RGB);
_xstart = _colstart;
// _ystart = _rowstart;
_ystart = 80;
break;
case 1:
writedata(ST7789_MADCTL_MY | ST7789_MADCTL_MV | ST7789_MADCTL_RGB);
_ystart = _colstart;
// _xstart = _rowstart;
_xstart = 80;
break;
case 2:
writedata(ST7789_MADCTL_RGB);
_xstart = _colstart;
_ystart = _rowstart;
break;
case 3:
writedata(ST7789_MADCTL_MX | ST7789_MADCTL_MV | ST7789_MADCTL_RGB);
_ystart = _colstart;
_xstart = _rowstart;
break;
}
}
void Arduino_ST7789::setAddrWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) {
setAddrWindow_int(x0,y0,x1-1,y1-1);
}
void Arduino_ST7789::setAddrWindow_int(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) {
uint16_t x_start = x0 + _xstart, x_end = x1 + _xstart;
uint16_t y_start = y0 + _ystart, y_end = y1 + _ystart;
writecommand(ST7789_CASET); // Column addr set
writedata(x_start >> 8);
writedata(x_start & 0xFF); // XSTART
writedata(x_end >> 8);
writedata(x_end & 0xFF); // XEND
writecommand(ST7789_RASET); // Row addr set
writedata(y_start >> 8);
writedata(y_start & 0xFF); // YSTART
writedata(y_end >> 8);
writedata(y_end & 0xFF); // YEND
writecommand(ST7789_RAMWR); // write to RAM
}
void Arduino_ST7789::drawPixel(int16_t x, int16_t y, uint16_t color) {
if((x < 0) ||(x >= _width) || (y < 0) || (y >= _height)) return;
setAddrWindow_int(x,y,x+1,y+1);
SPI_BEGIN_TRANSACTION();
DC_HIGH();
CS_LOW();
spiwrite(color >> 8);
spiwrite(color);
CS_HIGH();
SPI_END_TRANSACTION();
}
void Arduino_ST7789::drawFastVLine(int16_t x, int16_t y, int16_t h,
uint16_t color) {
// Rudimentary clipping
if((x >= _width) || (y >= _height)) return;
if((y+h-1) >= _height) h = _height-y;
setAddrWindow_int(x, y, x, y+h-1);
uint8_t hi = color >> 8, lo = color;
SPI_BEGIN_TRANSACTION();
DC_HIGH();
CS_LOW();
while (h--) {
spiwrite(hi);
spiwrite(lo);
}
CS_HIGH();
SPI_END_TRANSACTION();
}
void Arduino_ST7789::drawFastHLine(int16_t x, int16_t y, int16_t w,
uint16_t color) {
// Rudimentary clipping
if((x >= _width) || (y >= _height)) return;
if((x+w-1) >= _width) w = _width-x;
setAddrWindow_int(x, y, x+w-1, y);
uint8_t hi = color >> 8, lo = color;
SPI_BEGIN_TRANSACTION();
DC_HIGH();
CS_LOW();
while (w--) {
spiwrite(hi);
spiwrite(lo);
}
CS_HIGH();
SPI_END_TRANSACTION();
}
void Arduino_ST7789::fillScreen(uint16_t color) {
fillRect(0, 0, _width, _height, color);
}
// fill a rectangle
void Arduino_ST7789::fillRect(int16_t x, int16_t y, int16_t w, int16_t h,
uint16_t color) {
// rudimentary clipping (drawChar w/big text requires this)
if((x >= _width) || (y >= _height)) return;
if((x + w - 1) >= _width) w = _width - x;
if((y + h - 1) >= _height) h = _height - y;
setAddrWindow_int(x, y, x+w-1, y+h-1);
uint8_t hi = color >> 8, lo = color;
SPI_BEGIN_TRANSACTION();
DC_HIGH();
CS_LOW();
for(y=h; y>0; y--) {
for(x=w; x>0; x--) {
spiwrite(hi);
spiwrite(lo);
}
}
CS_HIGH();
SPI_END_TRANSACTION();
}
// Pass 8-bit (each) R,G,B, get back 16-bit packed color
uint16_t Arduino_ST7789::Color565(uint8_t r, uint8_t g, uint8_t b) {
return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3);
}
void Arduino_ST7789::invertDisplay(boolean i) {
writecommand(i ? ST7789_INVON : ST7789_INVOFF);
}
/******** low level bit twiddling **********/
inline void Arduino_ST7789::CS_HIGH(void) {
if(_cs>=0) {
#if defined(USE_FAST_IO)
*csport |= cspinmask;
#else
digitalWrite(_cs, HIGH);
#endif
}
}
inline void Arduino_ST7789::CS_LOW(void) {
if(_cs>=0) {
#if defined(USE_FAST_IO)
*csport &= ~cspinmask;
#else
digitalWrite(_cs, LOW);
#endif
}
}
inline void Arduino_ST7789::DC_HIGH(void) {
_DCbit = true;
#if defined(USE_FAST_IO)
*dcport |= dcpinmask;
#else
digitalWrite(_dc, HIGH);
#endif
}
inline void Arduino_ST7789::DC_LOW(void) {
_DCbit = false;
#if defined(USE_FAST_IO)
*dcport &= ~dcpinmask;
#else
digitalWrite(_dc, LOW);
#endif
}
void Arduino_ST7789::init(uint16_t width, uint16_t height) {
commonInit(NULL);
_colstart = ST7789_240x240_XSTART;
_rowstart = ST7789_240x240_YSTART;
_height = height;
_width = width;
displayInit(cmd_240x240);
setRotation(2);
}
void Arduino_ST7789::DisplayOnff(int8_t on) {
if (on) {
writecommand(ST7789_DISPON); //Display on
if (_bp>=0) {
#ifdef ST7789_DIMMER
ledcWrite(ESP32_PWM_CHANNEL,255);
#else
digitalWrite(_bp,HIGH);
#endif
}
} else {
writecommand(ST7789_DISPOFF);
if (_bp>=0) {
#ifdef ST7789_DIMMER
ledcWrite(ESP32_PWM_CHANNEL,0);
#else
digitalWrite(_bp,LOW);
#endif
}
}
}
// dimmer 0-100
void Arduino_ST7789::dim(uint8_t dimmer) {
if (dimmer>15) dimmer=15;
dimmer=((float)dimmer/15.0)*255.0;
#ifdef ESP32
ledcWrite(ESP32_PWM_CHANNEL,dimmer);
#endif
}
void Arduino_ST7789::pushColor(uint16_t color) {
SPI_BEGIN_TRANSACTION();
DC_HIGH();
CS_LOW();
spiwrite(color >> 8);
spiwrite(color);
CS_HIGH();
SPI_END_TRANSACTION();
}
void Arduino_ST7789::pushColors(uint16_t *data, uint8_t len, boolean first) {
uint16_t color;
SPI_BEGIN_TRANSACTION();
DC_HIGH();
CS_LOW();
while (len--) {
color = *data++;
spiwrite(color >> 8);
spiwrite(color);
}
CS_HIGH();
SPI_END_TRANSACTION();
}

View File

@ -0,0 +1,179 @@
/***************************************************
This is a library for the ST7789 IPS SPI display.
Originally written by Limor Fried/Ladyada for
Adafruit Industries.
Modified by Ananev Ilia
****************************************************/
#ifndef _ADAFRUIT_ST7789H_
#define _ADAFRUIT_ST7789H_
#include "Arduino.h"
#include "Print.h"
#include <Adafruit_GFX.h>
#include <renderer.h>
#if defined(__AVR__) || defined(CORE_TEENSY)
#include <avr/pgmspace.h>
#define USE_FAST_IO
typedef volatile uint8_t RwReg;
#elif defined(ARDUINO_STM32_FEATHER)
typedef volatile uint32 RwReg;
#define USE_FAST_IO
#elif defined(ARDUINO_FEATHER52)
typedef volatile uint32_t RwReg;
#define USE_FAST_IO
#elif defined(ESP8266)
#include <pgmspace.h>
#elif defined(__SAM3X8E__)
#undef __FlashStringHelper::F(string_literal)
#define F(string_literal) string_literal
#include <include/pio.h>
#define PROGMEM
#define pgm_read_byte(addr) (*(const unsigned char *)(addr))
#define pgm_read_word(addr) (*(const unsigned short *)(addr))
typedef unsigned char prog_uchar;
#endif
//#define SPI_HAS_TRANSACTION // already defined in SPI.h
#define ST7789_TFTWIDTH 240
#define ST7789_TFTHEIGHT 240
#define ST7789_240x240_XSTART 0
#define ST7789_240x240_YSTART 0
#define ST_CMD_DELAY 0x80 // special signifier for command lists
#define ST7789_NOP 0x00
#define ST7789_SWRESET 0x01
#define ST7789_RDDID 0x04
#define ST7789_RDDST 0x09
#define ST7789_SLPIN 0x10
#define ST7789_SLPOUT 0x11
#define ST7789_PTLON 0x12
#define ST7789_NORON 0x13
#define ST7789_INVOFF 0x20
#define ST7789_INVON 0x21
#define ST7789_DISPOFF 0x28
#define ST7789_DISPON 0x29
#define ST7789_CASET 0x2A
#define ST7789_RASET 0x2B
#define ST7789_RAMWR 0x2C
#define ST7789_RAMRD 0x2E
#define ST7789_PTLAR 0x30
#define ST7789_COLMOD 0x3A
#define ST7789_MADCTL 0x36
#define ST7789_MADCTL_MY 0x80
#define ST7789_MADCTL_MX 0x40
#define ST7789_MADCTL_MV 0x20
#define ST7789_MADCTL_ML 0x10
#define ST7789_MADCTL_RGB 0x00
#define ST7789_RDID1 0xDA
#define ST7789_RDID2 0xDB
#define ST7789_RDID3 0xDC
#define ST7789_RDID4 0xDD
// Color definitions
#undef BLACK
#define BLACK 0x0000
#define BLUE 0x001F
#define RED 0xF800
#define GREEN 0x07E0
#define CYAN 0x07FF
#define MAGENTA 0xF81F
#define YELLOW 0xFFE0
#undef WHITE
#define WHITE 0xFFFF
// Color definitions
#define ST7789_BLACK 0x0000 /* 0, 0, 0 */
#define ST7789_NAVY 0x000F /* 0, 0, 128 */
#define ST7789_DARKGREEN 0x03E0 /* 0, 128, 0 */
#define ST7789_DARKCYAN 0x03EF /* 0, 128, 128 */
#define ST7789_MAROON 0x7800 /* 128, 0, 0 */
#define ST7789_PURPLE 0x780F /* 128, 0, 128 */
#define ST7789_OLIVE 0x7BE0 /* 128, 128, 0 */
#define ST7789_LIGHTGREY 0xC618 /* 192, 192, 192 */
#define ST7789_DARKGREY 0x7BEF /* 128, 128, 128 */
#define ST7789_BLUE 0x001F /* 0, 0, 255 */
#define ST7789_GREEN 0x07E0 /* 0, 255, 0 */
#define ST7789_CYAN 0x07FF /* 0, 255, 255 */
#define ST7789_RED 0xF800 /* 255, 0, 0 */
#define ST7789_MAGENTA 0xF81F /* 255, 0, 255 */
#define ST7789_YELLOW 0xFFE0 /* 255, 255, 0 */
#define ST7789_WHITE 0xFFFF /* 255, 255, 255 */
#define ST7789_ORANGE 0xFD20 /* 255, 165, 0 */
#define ST7789_GREENYELLOW 0xAFE5 /* 173, 255, 47 */
#define ST7789_PINK 0xF81F
class Arduino_ST7789 : public Renderer {
public:
Arduino_ST7789(int8_t DC, int8_t RST, int8_t SID, int8_t SCLK, int8_t CS, int8_t bp);
Arduino_ST7789(int8_t DC, int8_t RST, int8_t CS, int8_t bp);
void setAddrWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1),
pushColor(uint16_t color),
fillScreen(uint16_t color),
drawPixel(int16_t x, int16_t y, uint16_t color),
drawFastVLine(int16_t x, int16_t y, int16_t h, uint16_t color),
drawFastHLine(int16_t x, int16_t y, int16_t w, uint16_t color),
fillRect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color),
setRotation(uint8_t r),
invertDisplay(boolean i),
DisplayInit(int8_t p,int8_t size,int8_t rot,int8_t font),
setAddrWindow_int(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1),
init(uint16_t width, uint16_t height);
uint16_t Color565(uint8_t r, uint8_t g, uint8_t b);
uint16_t GetColorFromIndex(uint8_t index);
uint16_t color565(uint8_t r, uint8_t g, uint8_t b) { return Color565(r, g, b); }
void DisplayOnff(int8_t on);
void dim(uint8_t contrast);
void pushColors(uint16_t *data, uint8_t len, boolean first);
protected:
uint8_t _colstart, _rowstart, _xstart, _ystart; // some displays need this changed
void displayInit(const uint8_t *addr);
void spiwrite(uint8_t),
writecommand(uint8_t c),
writedata(uint8_t d),
commonInit(const uint8_t *cmdList);
private:
inline void CS_HIGH(void);
inline void CS_LOW(void);
inline void DC_HIGH(void);
inline void DC_LOW(void);
boolean _hwSPI;
boolean _SPI9bit;
boolean _DCbit;
int8_t _cs, _dc, _rst, _sid, _sclk, _bp;
#if defined(USE_FAST_IO)
volatile RwReg *dataport, *clkport, *csport, *dcport;
#if defined(__AVR__) || defined(CORE_TEENSY) // 8 bit!
uint8_t datapinmask, clkpinmask, cspinmask, dcpinmask;
#else // 32 bit!
uint32_t datapinmask, clkpinmask, cspinmask, dcpinmask;
#endif
#endif
};
#endif

View File

@ -0,0 +1,3 @@
This is a library for the ST7789 IPS SPI display.
Also requires the Adafruit_GFX library for Arduino.

View File

@ -0,0 +1,278 @@
/***************************************************
This is a library for the ST7789 IPS SPI display.
Originally written by Limor Fried/Ladyada for
Adafruit Industries.
Modified by Ananev Ilia
****************************************************/
#include <Adafruit_GFX.h> // Core graphics library by Adafruit
#include <Arduino_ST7789.h> // Hardware-specific library for ST7789 (with or without CS pin)
#include <SPI.h>
#define TFT_DC 8
#define TFT_RST 9
#define TFT_CS 10 // only for displays with CS pin
#define TFT_MOSI 11 // for hardware SPI data pin (all of available pins)
#define TFT_SCLK 13 // for hardware SPI sclk pin (all of available pins)
//You can use different type of hardware initialization
//using hardware SPI (11, 13 on UNO; 51, 52 on MEGA; ICSP-4, ICSP-3 on DUE and etc)
//Arduino_ST7789 tft = Arduino_ST7789(TFT_DC, TFT_RST); //for display without CS pin
//Arduino_ST7789 tft = Arduino_ST7789(TFT_DC, TFT_RST, TFT_CS); //for display with CS pin
//or you can use software SPI on all available pins (slow)
//Arduino_ST7789 tft = Arduino_ST7789(TFT_DC, TFT_RST, TFT_MOSI, TFT_SCLK); //for display without CS pin
//Arduino_ST7789 tft = Arduino_ST7789(TFT_DC, TFT_RST, TFT_MOSI, TFT_SCLK, TFT_CS); //for display with CS pin
Arduino_ST7789 tft = Arduino_ST7789(-1, TFT_RST, TFT_MOSI, TFT_SCLK, TFT_CS); //for display with CS pin and DC via 9bit SPI
float p = 3.1415926;
void setup(void) {
Serial.begin(9600);
Serial.print("Hello! ST7789 TFT Test");
tft.init(240, 240); // initialize a ST7789 chip, 240x240 pixels
Serial.println("Initialized");
uint16_t time = millis();
tft.fillScreen(BLACK);
time = millis() - time;
Serial.println(time, DEC);
delay(500);
// large block of text
tft.fillScreen(BLACK);
testdrawtext("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur adipiscing ante sed nibh tincidunt feugiat. Maecenas enim massa, fringilla sed malesuada et, malesuada sit amet turpis. Sed porttitor neque ut ante pretium vitae malesuada nunc bibendum. Nullam aliquet ultrices massa eu hendrerit. Ut sed nisi lorem. In vestibulum purus a tortor imperdiet posuere. ", WHITE);
delay(1000);
// tft print function
tftPrintTest();
delay(4000);
// a single pixel
tft.drawPixel(tft.width()/2, tft.height()/2, GREEN);
delay(500);
// line draw test
testlines(YELLOW);
delay(500);
// optimized lines
testfastlines(RED, BLUE);
delay(500);
testdrawrects(GREEN);
delay(500);
testfillrects(YELLOW, MAGENTA);
delay(500);
tft.fillScreen(BLACK);
testfillcircles(10, BLUE);
testdrawcircles(10, WHITE);
delay(500);
testroundrects();
delay(500);
testtriangles();
delay(500);
mediabuttons();
delay(500);
Serial.println("done");
delay(1000);
}
void loop() {
tft.invertDisplay(true);
delay(500);
tft.invertDisplay(false);
delay(500);
}
void testlines(uint16_t color) {
tft.fillScreen(BLACK);
for (int16_t x=0; x < tft.width(); x+=6) {
tft.drawLine(0, 0, x, tft.height()-1, color);
}
for (int16_t y=0; y < tft.height(); y+=6) {
tft.drawLine(0, 0, tft.width()-1, y, color);
}
tft.fillScreen(BLACK);
for (int16_t x=0; x < tft.width(); x+=6) {
tft.drawLine(tft.width()-1, 0, x, tft.height()-1, color);
}
for (int16_t y=0; y < tft.height(); y+=6) {
tft.drawLine(tft.width()-1, 0, 0, y, color);
}
tft.fillScreen(BLACK);
for (int16_t x=0; x < tft.width(); x+=6) {
tft.drawLine(0, tft.height()-1, x, 0, color);
}
for (int16_t y=0; y < tft.height(); y+=6) {
tft.drawLine(0, tft.height()-1, tft.width()-1, y, color);
}
tft.fillScreen(BLACK);
for (int16_t x=0; x < tft.width(); x+=6) {
tft.drawLine(tft.width()-1, tft.height()-1, x, 0, color);
}
for (int16_t y=0; y < tft.height(); y+=6) {
tft.drawLine(tft.width()-1, tft.height()-1, 0, y, color);
}
}
void testdrawtext(char *text, uint16_t color) {
tft.setCursor(0, 0);
tft.setTextColor(color);
tft.setTextWrap(true);
tft.print(text);
}
void testfastlines(uint16_t color1, uint16_t color2) {
tft.fillScreen(BLACK);
for (int16_t y=0; y < tft.height(); y+=5) {
tft.drawFastHLine(0, y, tft.width(), color1);
}
for (int16_t x=0; x < tft.width(); x+=5) {
tft.drawFastVLine(x, 0, tft.height(), color2);
}
}
void testdrawrects(uint16_t color) {
tft.fillScreen(BLACK);
for (int16_t x=0; x < tft.width(); x+=6) {
tft.drawRect(tft.width()/2 -x/2, tft.height()/2 -x/2 , x, x, color);
}
}
void testfillrects(uint16_t color1, uint16_t color2) {
tft.fillScreen(BLACK);
for (int16_t x=tft.width()-1; x > 6; x-=6) {
tft.fillRect(tft.width()/2 -x/2, tft.height()/2 -x/2 , x, x, color1);
tft.drawRect(tft.width()/2 -x/2, tft.height()/2 -x/2 , x, x, color2);
}
}
void testfillcircles(uint8_t radius, uint16_t color) {
for (int16_t x=radius; x < tft.width(); x+=radius*2) {
for (int16_t y=radius; y < tft.height(); y+=radius*2) {
tft.fillCircle(x, y, radius, color);
}
}
}
void testdrawcircles(uint8_t radius, uint16_t color) {
for (int16_t x=0; x < tft.width()+radius; x+=radius*2) {
for (int16_t y=0; y < tft.height()+radius; y+=radius*2) {
tft.drawCircle(x, y, radius, color);
}
}
}
void testtriangles() {
tft.fillScreen(BLACK);
int color = 0xF800;
int t;
int w = tft.width()/2;
int x = tft.height()-1;
int y = 0;
int z = tft.width();
for(t = 0 ; t <= 15; t++) {
tft.drawTriangle(w, y, y, x, z, x, color);
x-=4;
y+=4;
z-=4;
color+=100;
}
}
void testroundrects() {
tft.fillScreen(BLACK);
int color = 100;
int i;
int t;
for(t = 0 ; t <= 4; t+=1) {
int x = 0;
int y = 0;
int w = tft.width()-2;
int h = tft.height()-2;
for(i = 0 ; i <= 16; i+=1) {
tft.drawRoundRect(x, y, w, h, 5, color);
x+=2;
y+=3;
w-=4;
h-=6;
color+=1100;
}
color+=100;
}
}
void tftPrintTest() {
tft.setTextWrap(false);
tft.fillScreen(BLACK);
tft.setCursor(0, 30);
tft.setTextColor(RED);
tft.setTextSize(1);
tft.println("Hello World!");
tft.setTextColor(YELLOW);
tft.setTextSize(2);
tft.println("Hello World!");
tft.setTextColor(GREEN);
tft.setTextSize(3);
tft.println("Hello World!");
tft.setTextColor(BLUE);
tft.setTextSize(4);
tft.print(1234.567);
delay(1500);
tft.setCursor(0, 0);
tft.fillScreen(BLACK);
tft.setTextColor(WHITE);
tft.setTextSize(0);
tft.println("Hello World!");
tft.setTextSize(1);
tft.setTextColor(GREEN);
tft.print(p, 6);
tft.println(" Want pi?");
tft.println(" ");
tft.print(8675309, HEX); // print 8,675,309 out in HEX!
tft.println(" Print HEX!");
tft.println(" ");
tft.setTextColor(WHITE);
tft.println("Sketch has been");
tft.println("running for: ");
tft.setTextColor(MAGENTA);
tft.print(millis() / 1000);
tft.setTextColor(WHITE);
tft.print(" seconds.");
}
void mediabuttons() {
// play
tft.fillScreen(BLACK);
tft.fillRoundRect(25, 10, 78, 60, 8, WHITE);
tft.fillTriangle(42, 20, 42, 60, 90, 40, RED);
delay(500);
// pause
tft.fillRoundRect(25, 90, 78, 60, 8, WHITE);
tft.fillRoundRect(39, 98, 20, 45, 5, GREEN);
tft.fillRoundRect(69, 98, 20, 45, 5, GREEN);
delay(500);
// play color
tft.fillTriangle(42, 20, 42, 60, 90, 40, BLUE);
delay(50);
// pause color
tft.fillRoundRect(39, 98, 20, 45, 5, RED);
tft.fillRoundRect(69, 98, 20, 45, 5, RED);
// play color
tft.fillTriangle(42, 20, 42, 60, 90, 40, GREEN);
}

View File

@ -0,0 +1,30 @@
#######################################
# Syntax Coloring Map
#######################################
#######################################
# Datatypes (KEYWORD1)
#######################################
ST7789 KEYWORD1
#######################################
# Methods and Functions (KEYWORD2)
#######################################
setRotation KEYWORD2
setAddrWindow KEYWORD2
pushColor KEYWORD2
drawPixel KEYWORD2
drawFastVLine KEYWORD2
drawFastHLine KEYWORD2
fillRect KEYWORD2
setRotation KEYWORD2
setRotation KEYWORD2
height KEYWORD2
width KEYWORD2
invertDisplay KEYWORD2
drawImage KEYWORD2
setScrollArea KEYWORD2
scroll KEYWORD2

View File

@ -0,0 +1,9 @@
name=Arduino ST7789 Library
version=0.9.5
author=Ananev Ilya
maintainer=Ananev Ilya <ananevilya@gmail.com>
sentence=This is a library for the ST7789 IPS SPI display.
paragraph=This is a library for the ST7789 IPS SPI display.
category=Display
url=https://github.com/ananevilya/Arduino-ST7789-Library
architectures=*

View File

@ -0,0 +1,88 @@
# Run whenever a PR is generated or updated.
# Most jobs check out the code, ensure Python3 is installed, and for build
# tests the ESP8266 toolchain is cached when possible to speed up execution.
name: ESP8266Audio
on:
push:
branches:
- master
pull_request:
jobs:
build-esp8266:
name: Build ESP8266
runs-on: ubuntu-latest
strategy:
matrix:
chunk: [0, 1, 2, 3, 4]
steps:
- uses: actions/checkout@v2
with:
submodules: true
- uses: actions/setup-python@v2
with:
python-version: '3.x'
- name: Build Sketches
env:
TRAVIS_BUILD_DIR: ${{ github.workspace }}
TRAVIS_TAG: ${{ github.ref }}
BUILD_TYPE: build
BUILD_MOD: 5
BUILD_REM: ${{ matrix.chunk }}
run: |
bash ./tests/common.sh
build-esp32:
name: Build ESP-32
runs-on: ubuntu-latest
strategy:
matrix:
chunk: [0, 1, 2, 3, 4]
steps:
- uses: actions/checkout@v2
with:
submodules: true
- uses: actions/setup-python@v2
with:
python-version: '3.x'
- name: Build Sketches
env:
TRAVIS_BUILD_DIR: ${{ github.workspace }}
TRAVIS_TAG: ${{ github.ref }}
BUILD_TYPE: build_esp32
BUILD_MOD: 5
BUILD_REM: ${{ matrix.chunk }}
run: |
bash ./tests/common.sh
# Run host test suite under valgrind for runtime checking of code.
host-tests:
name: Host tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
submodules: true
- uses: actions/setup-python@v2
with:
python-version: '3.x'
- name: Run host tests
env:
TRAVIS_BUILD_DIR: ${{ github.workspace }}
TRAVIS_TAG: ${{ github.ref }}
run: |
sudo dpkg --add-architecture i386
sudo apt-get update
sudo apt-get install valgrind lcov gcc-multilib g++-multilib libc6-dbg:i386
cd ./tests/host/
make
valgrind --leak-check=full --track-origins=yes -v --error-limit=no --show-leak-kinds=all --error-exitcode=999 ./mp3
valgrind --leak-check=full --track-origins=yes -v --error-limit=no --show-leak-kinds=all --error-exitcode=999 ./aac
valgrind --leak-check=full --track-origins=yes -v --error-limit=no --show-leak-kinds=all --error-exitcode=999 ./wav
valgrind --leak-check=full --track-origins=yes -v --error-limit=no --show-leak-kinds=all --error-exitcode=999 ./midi

674
lib/ESP8266Audio/LICENSE Executable file
View File

@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
{one line to give the program's name and a brief idea of what it does.}
Copyright (C) {year} {name of author}
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
{project} Copyright (C) {year} {fullname}
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<http://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.

264
lib/ESP8266Audio/README.md Executable file
View File

@ -0,0 +1,264 @@
# ESP8266Audio - supports ESP8266 & ESP32 [![Gitter](https://badges.gitter.im/ESP8266Audio/community.svg)](https://gitter.im/ESP8266Audio/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
Arduino library for parsing and decoding MOD, WAV, MP3, FLAC, MIDI, AAC, and RTTL files and playing them on an I2S DAC or even using a software-simulated delta-sigma DAC with dynamic 32x-128x oversampling.
ESP8266 is fully supported and most mature, but ESP32 is also mostly there with built-in DAC as well as external ones.
For real-time, autonomous speech synthesis, check out [ESP8266SAM](https://github.com/earlephilhower/ESP8266SAM), a library which uses this one and a port of an ancient format-based synthesis program to allow your ESP8266 to talk with low memory and no network required.
## Disclaimer
All this code is released under the GPL, and all of it is to be used at your own risk. If you find any bugs, please let me know via the GitHub issue tracker or drop me an email. The MOD and MP3 routines were taken from StellaPlayer and libMAD respectively. The software I2S delta-sigma 32x oversampling DAC was my own creation, and sounds quite good if I do say so myself.
The AAC decode code is from the Helix project and licensed under RealNetwork's RSPL license. For commercial use you're still going to need the usual AAC licensing from [Via Licensing](http://www.via-corp.com/us/en/licensing/aac/overview.html).
On the ESP32, AAC-SBR is supported (many webradio stations use this to reduce bandwidth even further). The ESP8266, however, does not support it due to a lack of onboard RAM.
MIDI decoding comes from a highly ported [MIDITONES](https://github.com/LenShustek/miditones) combined with a massively memory-optimized [TinySoundFont](https://github.com/schellingb/TinySoundFont), see the respective source files for more information.
Opus, OGG, and OpusFile are from [Xiph.org](https://xiph.org) with the Xiph license and patent described in src/{opusfile,libggg,libopus}/COPYING.. **NOTE** Opus decoding currently only works on the ESP32 due to the large memory requirements of opusfile. PRs to rewrite it to be less memory intensive would be much appreciated.
## Neat Things People Have Done With ESP8266Audio
If you have a neat use for this library, [I'd love to hear about it](mailto:earlephilhower@yahoo.com)!
My personal use of the ESP8266Audio library is only to drive a 3D-printed, network-time-setting alarm clock for my kids which can play an MP3 instead of a bell to wake them up, called [Psychoclock](https://github.com/earlephilhower/psychoclock).
Harald Sattler has built a neat German [word clock with MP3 alarm](http://www.harald-sattler.de/html/mini-wecker.htm). Detailed discussion on the process and models are included.
Erich Heinemann has developed a Stomper (instrument for playing samples in real-time during a live stage performance) that you can find more info about [here](https://github.com/ErichHeinemann/hman-stomper).
Dagnall53 has integrated this into a really neat MQTT based model train controller to add sounds to his set. More info is available [here](https://github.com/dagnall53/ESPMQTTRocnetSound), including STL files for 3D printed components!
JohannesMTC has built a similar project especially for model trains: https://github.com/JohannesMTC/ESP32_MAS
A neat MQTT-driven ESP8266 light-and-sound device (alarm? toy? who can say!) was built by @CosmicMac, available at https://github.com/CosmicMac/ESParkle
A very interesting "linear clock" with a stepper motor, NTP time keeping, and configurable recorded chimes with schematics, 3D printer plans, and source code, is now available http://home.kpn.nl/bderogee1980/projects/linear_clock/linear_clock.html
## Prerequisites
First, make sure you are running the 2.6.3/later or GIT head version of the Arduino libraries for ESP8266, or the latest ESP32 SDK from Espressif.
You can use GIT to pull right from GitHub: see [this README](https://github.com/esp8266/Arduino/blob/master/README.md#using-git-version) for detailed instructions.
## Installation
Install the library in your ~/Arduino/libraries
```sh
mkdir -p ~/Arduino/libraries
cd ~/Arduino/libraries
git clone https://github.com/earlephilhower/ESP8266Audio
```
When in the IDE please select the following options on the ESP8266:
```
Tools->lwIP Variant->v1.4 Open Source, or V2 Higher Bandwidth
Tools->CPU Frequency->160MHz
```
## Usage
Create an AudioInputXXX source pointing to your input file, an AudioOutputXXX sink as either an I2S, I2S-sw-DAC, or as a "SerialWAV" which simply writes a WAV file to the Serial port which can be dumped to a file on your development system, and an AudioGeneratorXXX to actually take that input and decode it and send to the output.
After creation, you need to call the AudioGeneratorXXX::loop() routine from inside your own main loop() one or more times. This will automatically read as much of the file as needed and fill up the I2S buffers and immediately return. Since this is not interrupt driven, if you have large delay()s in your code, you may end up with hiccups in playback. Either break large delays into very small ones with calls to AudioGenerator::loop(), or reduce the sampling rate to require fewer samples per second.
## Example
See the examples directory for some simple examples, but the following snippet can play an MP3 file over the simulated I2S DAC:
```cpp
#include <Arduino.h>
#include "AudioFileSourceSPIFFS.h"
#include "AudioGeneratorMP3.h"
#include "AudioOutputI2SNoDAC.h"
AudioGeneratorMP3 *mp3;
AudioFileSourceSPIFFS *file;
AudioOutputI2SNoDAC *out;
void setup()
{
Serial.begin(115200);
delay(1000);
SPIFFS.begin();
file = new AudioFileSourceSPIFFS("/jamonit.mp3");
out = new AudioOutputI2SNoDAC();
mp3 = new AudioGeneratorMP3();
mp3->begin(file, out);
}
void loop()
{
if (mp3->isRunning()) {
if (!mp3->loop()) mp3->stop();
} else {
Serial.printf("MP3 done\n");
delay(1000);
}
}
```
## AudioFileSource classes
AudioFileSource: Base class which implements a very simple read-only "file" interface. Required because it seems everyone has invented their own filesystem on the Arduino with their own unique twist. Using this wrapper lets that be abstracted and makes the AudioGenerator simpler as it only calls these simple functions.
AudioFileSourceSPIFFS: Reads a file from the SPIFFS filesystem
AudioFileSourcePROGMEM: Reads a file from a PROGMEM array. Under UNIX you can use "xxd -i file.mp3 > file.h" to get the basic format, then add "const" and "PROGMEM" to the generated array and include it in your sketch. See the example .h files for a concrete example.
AudioFileSourceHTTPStream: Simple implementation of a streaming HTTP reader for ShoutCast-type MP3 streaming. Not yet resilient, and at 44.1khz 128bit stutters due to CPU limitations, but it works more or less.
## AudioFileSourceBuffer - Double buffering, useful for HTTP streams
AudioFileSourceBuffer is an input source that simpy adds an additional RAM buffer of the output of any other AudioFileSource. This is particularly useful for web streaming where you need to have 1-2 packets in memory to ensure hiccup-free playback.
Create your standard input file source, create the buffer with the original source as its input, and pass this buffer object to the generator.
```cpp
...
AudioGeneratorMP3 *mp3;
AudioFileSourceHTTPStream *file;
AudioFileSourceBuffer *buff;
AudioOutputI2SNoDAC *out;
...
// Create the HTTP stream normally
file = new AudioFileSourceHTTPStream("http://your.url.here/mp3");
// Create a buffer using that stream
buff = new AudioFileSourceBuffer(file, 2048);
out = new AudioOutputI2SNoDAC();
mp3 = new AudioGeneratorMP3();
// Pass in the *buffer*, not the *http stream* to enable buffering
mp3->begin(buff, out);
...
```
## AudioFileSourceID3 - ID3 stream parser filter with a user-specified callback
This class, which takes as input any other AudioFileSource and outputs an AudioFileSource suitable for any decoder, automatically parses out ID3 tags from MP3 files. You need to specify a callback function, which will be called as tags are decoded and allow you to update your UI state with this information. See the PlayMP3FromSPIFFS example for more information.
## AudioGenerator classes
AudioGenerator: Base class for all file decoders. Takes a AudioFileSource and an AudioOutput object to get the data from and to write decoded samples to. Call its loop() function as often as you can to ensure the buffers are always kept full and your music won't skip.
AudioGeneratorWAV: Reads and plays Microsoft WAVE (.WAV) format files of 8 or 16 bits.
AudioGeneratorMOD: Reads and plays Amiga ModTracker files (.MOD). Use a 160MHz clock as this requires tons of SPIFFS reads (which are painfully slow) to get raw instrument sample data for every output sample. See https://modarchive.org for many free MOD files.
AudioGeneratorMP3: Reads and plays MP3 format files (.MP3) using a ported libMAD library. Use a 160MHz clock to ensure enough compute power to decode 128KBit 44.1KHz without hiccups. For complete porting history with the gory details, look at https://github.com/earlephilhower/libmad-8266
AudioGeneratorFLAC: Plays FLAC files via ported libflac-1.3.2. On the order of 30KB heap and minimal stack required as-is.
AudioGeneratorMIDI: Plays a MIDI file using a wavetable synthesizer and a SoundFont2 wavetable input. Theoretically up to 16 simultaneous notes available, but depending on the memory needed for the SF2 structures you may not be able to get that many before hitting OOM.
AudioGeneratorAAC: Requires about 30KB of heap and plays a mono or stereo AAC file using the Helix fixed-point AAC decoder.
AudioGeneratorRTTTL: Enjoy the pleasures of monophonic, 4-octave ringtones on your ESP8266. Very low memory and CPU requirements for simple tunes.
## AudioOutput classes
AudioOutput: Base class for all output drivers. Takes a sample at a time and returns true/false if there is buffer space for it. If it returns false, it is the calling object's (AudioGenerator's) job to keep the data that didn't fit and try again later.
AudioOutputI2S: Interface for any I2S 16-bit DAC. Sends stereo or mono signals out at whatever frequency set. Tested with Adafruit's I2SDAC and a Beyond9032 DAC from eBay. Tested up to 44.1KHz. To use the internal DAC on ESP32, instantiate this class as `AudioOutputI2S(0,1)`, see example `PlayMODFromPROGMEMToDAC` and code in [AudioOutputI2S.cpp](src/AudioOutputI2S.cpp#L29) for details.
AudioOutputI2SNoDAC: Abuses the I2S interface to play music without a DAC. Turns it into a 32x (or higher) oversampling delta-sigma DAC. Use the schematic below to drive a speaker or headphone from the I2STx pin (i.e. Rx). Note that with this interface, depending on the transistor used, you may need to disconnect the Rx pin from the driver to perform serial uploads. Mono-only output, of course.
AudioOutputSPDIF (experimental): Another way to abuse the I2S peripheral to send out BMC encoded S/PDIF bitstream. To interface with S/PDIF receiver it needs optical or coaxial transceiver, for which some examples can be found at https://www.epanorama.net/documents/audio/spdif.html. It should work even with the simplest form with red LED and current limiting resistor, fed into TOSLINK cable. Minimum sample rate supported by is 32KHz. Due to BMC coding, actual symbol rate on the pin is 4x normal I2S data rate, which drains DMA buffers quickly. See more details inside [AudioOutputSPDIF.cpp](src/AudioOutputSPDIF.cpp#L17)
AudioOutputSerialWAV: Writes a binary WAV format with headers to the Serial port. If you capture the serial output to a file you can play it back on your development system.
AudioOutputSPIFFSWAV: Writes a binary WAV format with headers to a SPIFFS filesystem. Ensure the FS is mounted and SPIFFS is started before calling. USe the SetFilename() call to pick the output file before starting.
AudioOutputNull: Just dumps samples to /dev/null. Used for speed testing as it doesn't artificially limit the AudioGenerator output speed since there are no buffers to fill/drain.
## I2S DACs
I've used both the Adafruit [I2S +3W amp DAC](https://www.adafruit.com/product/3006) and a generic PCM5102 based DAC with success. The biggest problems I've seen from users involve pinouts from the ESP8266 for GPIO and hooking up all necessary pins on the DAC board.
### Adafruit I2S DAC
This is quite simple and only needs the GND, VIN, LRC, BCLK< and DIN pins to be wired. Be sure to use +5V on the VIN to get the loudest sound. See the [Adafruit example page](https://learn.adafruit.com/adafruit-max98357-i2s-class-d-mono-amp) for more info.
### PCM5102 DAC
I've used several versions of PCM5102 DAC boards purchased from eBay. They've all had the same pinout, no matter the form factor. There are several input configuration pins beyond the I2S interface itself that need to be wired:
* 3.3V from ESP8266 -> VCC, 33V, XMT
* GND from ESP8266 -> GND, FLT, DMP, FMT, SCL
* (Standard I2S interface) BCLK->BCK, I2SO->DIN, and LRCLK(WS)->LCK
### Others
There are many other variants out there, and they should all work reasonably well with this code and the ESP8266. Please be certain you've read the datasheet and are applying proper input voltages, and be sure to tie off any unused inputs to GND or VCC as appropriate. LEaving an input pin floating on any integrated circuit can cause unstable operation as it may pick up noise from the environment (very low input capacitance) and cause havoc with internal IC settings.
## Software I2S Delta-Sigma DAC (i.e. playing music with a single transistor and speaker)
For the best fidelity, and stereo to boot, spend the money on a real I2S DAC. Adafruit makes a great mono one with amplifier, and you can find stereo unamplified ones on eBay or elsewhere quite cheaply. However, thanks to the software delta-sigma DAC with 32x oversampling (up to 128x if the audio rate is low enough) you can still have pretty good sound!
Use the `AudioOutputI2S*No*DAC` object instead of the `AudioOutputI2S` in your code, and the following schematic to drive a 2-3W speaker using a single $0.05 NPN 2N3904 transistor and ~1K resistor:
```
2N3904 (NPN)
+---------+
| | +-|
| E B C | / S|
+-|--|--|-+ | P|
| | +------+ E|
| | | A|
ESP8266-GND ------------------+ | +------+ K|
| | | E|
ESP8266-I2SOUT (Rx) -----/\/\/\--+ | \ R|
| +-|
USB 5V -----------------------------+
You may also want to add a 220uF cap from USB5V to GND just to help filter out any voltage droop during high volume playback.
```
If you don't have a 5V source available on your ESP model, you can use the 5V from your USB serial adapter, or even the 3V from the ESP8266 (but it'll be lower volume). Don't try and drive the speaker without the transistor, the ESP8266 pins can't give enough current to drive even a headphone well and you may end up damaging your device.
Connections are as a follows:
```
ESP8266-RX(I2S tx) -- Resistor (~1K ohm, not critical) -- 2N3904 Base
ESP8266-GND -- 2N3904 Emitter
USB-5V -- Speaker + Terminal
2N3904-Collector -- Speaker - Terminal
```
*NOTE*: A prior version of this schematic had a direct connection from the ESP8266 to the base of the transistor. While this does provide the maximum amplitude, it also can draw more current from the 8266 than is safe, and can also cause the transistor to overheat.
As of the latest ESP8266Audio release, with the software delta-sigma DAC the LRCLK and BCLK pins *can* be used by an application. Simply use normal `pinMode` and `dicitalWrite` or `digitalRead` as desired.
### High pitched buzzing with the 1-T circuit
The 1-T amp can _NOT_ drive any sort of amplified speaker. If there is a power or USB input to the speaker, or it has lights or Bluetooth or a battery, it can _NOT_ be used with this circuit.
The 1T output is a binary signal at 0 or 5V, with nothing in between. When you connect to a 8ohm paper physical speaker directly, the speaker cone itself has inertia and acts as a low pass filter and averages the density of pulses in order to give a nice, analog output.
When you feed the 1T output to an amp you are alternatively grounding and overdriving the op-amp's input at a high frequency. That causes ringing and the opamp has a frequency response high enough to amplify the high frequency noise and you get that buzzing.
The same problem may happen with piezo speakers. They have a very high frequency response, normally, and have (almost) no inertia. So you hear the buzzing at high frequency.
You could attach the 1T output to a low pass and feed that into an amplifier. But at that point it is easier to just get an I2S DAC and avoid the whole thing (plus get stereo and true 16-bit output).
### Debugging the 1-T amp circuit, compliments of @msmcmickey
If you've built the amp but are not getting any sound, @msmcmickey wrote up a very good debugging sequence to check:
0. Please double-check the wiring. GPIO pins and board pins are not always the same and vary immensely between brands of ESP8266 carrier boards.
1. Is the transistor connected properly? Check the datasheet for this package style and make sure you have the leads connected properly. This package has three leads, and the lead that is by itself in the middle of the one side is the collector, not the base as you might expect it to be.
2. If connected properly, do you have ~5 volts between the collector and emitter?
3. Was the transistor possibly damaged/overheated during soldering, or by connecting it improperly? Out-of-circuit diode check voltage drop test using a multimeter from base->emitter and base->collector should be between .5 and .7 volts. If it's shorted or open or conducting in both directions, then replace it and make sure it's connected properly.
## SPDIF optical output
The proper way would be using optical TOSLINK transmitter (i.e. TOTXxxx). For testing, you can try with ~660nm red LED and resistor. Same as your basic Blink project with external LED, just that the LED will blink a bit faster.
```
____
ESP Pin -------|____|--------+
|
---
V LED
---
|
Ground ---------------------+
```
For ESP8266 with red LED (~1.9Vf drop) you need minimum 150Ohm resistor (12mA max per pin), and output pin is fixed (GPIO3/RX0).On ESP32 it is confgurable with `AudioOutputSPDIF(gpio_num)`.
## Using external SPI RAM to increase buffer
A class allows you to use a 23lc1024 SPI RAM from Microchip as input buffer. This chip connects to ESP8266 HSPI port and provides a large buffer to help avoid hiccus in playback of web streams.
The current version allows for using the standard hardware CS (GPIO15) or any other pin via software at slightly less performance. The following schematic shows one example:
![Example of SPIRAM Schematic](examples/StreamMP3FromHTTP_SPIRAM/Schema_Spiram.png)
## Notes for using SD cards and ESP8266Audio on Wemos shields
I've been told the Wemos SD card shield uses GPIO15 as the SD chip select. This needs to be changed because GPIO15 == I2SBCLK, and is driven even if you're using the NoDAC option. Once you move the CS to another pin and update your program it should work fine.
## Porting to other microcontrollers
There's no ESP8266-specific code in the AudioGenerator routines, so porting to other controllers should be relatively easy assuming they have the same endianness as the Xtensa core used. Drop me a line if you're doing this, I may be able to help point you in the right direction.
## Thanks
Thanks to the authors of StellaPlayer and libMAD for releasing their code freely, and to the maintainers and contributors to the ESP8266 Arduino port.
Also, big thanks to @tueddy for getting the initial ESP32 porting into the tree!
-Earle F. Philhower, III
earlephilhower@yahoo.com

View File

@ -0,0 +1,68 @@
#include <Arduino.h>
#ifdef ESP32
#include <WiFi.h>
#else
#include <ESP8266WiFi.h>
#endif
#include "AudioFileSourcePROGMEM.h"
#include "AudioGeneratorWAV.h"
#include "AudioOutputI2S.h"
#include "AudioOutputMixer.h"
// VIOLA sample taken from https://ccrma.stanford.edu/~jos/pasp/Sound_Examples.html
#include "viola.h"
AudioGeneratorWAV *wav[2];
AudioFileSourcePROGMEM *file[2];
AudioOutputI2S *out;
AudioOutputMixer *mixer;
AudioOutputMixerStub *stub[2];
void setup()
{
WiFi.mode(WIFI_OFF);
Serial.begin(115200);
delay(1000);
Serial.printf("WAV start\n");
audioLogger = &Serial;
file[0] = new AudioFileSourcePROGMEM( viola, sizeof(viola) );
out = new AudioOutputI2S();
mixer = new AudioOutputMixer(32, out);
stub[0] = mixer->NewInput();
stub[0]->SetGain(0.3);
wav[0] = new AudioGeneratorWAV();
wav[0]->begin(file[0], stub[0]);
// Begin wav[1] later in loop()
Serial.printf("starting 1\n");
}
void loop()
{
static uint32_t start = 0;
static bool go = false;
if (!start) start = millis();
if (wav[0]->isRunning()) {
if (!wav[0]->loop()) { wav[0]->stop(); stub[0]->stop(); Serial.printf("stopping 1\n"); }
}
if (millis()-start > 3000) {
if (!go) {
Serial.printf("starting 2\n");
stub[1] = mixer->NewInput();
stub[1]->SetGain(0.4);
wav[1] = new AudioGeneratorWAV();
file[1] = new AudioFileSourcePROGMEM( viola, sizeof(viola) );
wav[1]->begin(file[1], stub[1]);
go = true;
}
if (wav[1]->isRunning()) {
if (!wav[1]->loop()) { wav[1]->stop(); stub[1]->stop(); Serial.printf("stopping 2\n");}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,33 @@
#include <Arduino.h>
#include "AudioGeneratorAAC.h"
#include "AudioOutputI2S.h"
#include "AudioFileSourcePROGMEM.h"
#include "sampleaac.h"
AudioFileSourcePROGMEM *in;
AudioGeneratorAAC *aac;
AudioOutputI2S *out;
void setup()
{
Serial.begin(115200);
audioLogger = &Serial;
in = new AudioFileSourcePROGMEM(sampleaac, sizeof(sampleaac));
aac = new AudioGeneratorAAC();
out = new AudioOutputI2S();
aac->begin(in, out);
}
void loop()
{
if (aac->isRunning()) {
aac->loop();
} else {
Serial.printf("AAC done\n");
delay(1000);
}
}

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,68 @@
#include <Arduino.h>
#include "AudioFileSourceSD.h"
#include "AudioOutputSPDIF.h"
#include "AudioGeneratorFLAC.h"
// For this sketch, you need connected SD card with '.flac' music files in the root
// directory. Some samples with various sampling rates are available from i.e.
// Espressif Audio Development Framework at:
// https://docs.espressif.com/projects/esp-adf/en/latest/design-guide/audio-samples.html
//
// On ESP8266 you might need to reencode FLAC files with max '-2' compression level
// (i.e. 1152 maximum block size) or you will run out of memory. FLAC files will be
// slightly bigger but you don't loose audio quality with reencoding (lossles codec).
// You may need a fast SD card. Set this as high as it will work (40MHz max).
#define SPI_SPEED SD_SCK_MHZ(40)
// On ESP32 you can adjust the SPDIF_OUT_PIN (GPIO number).
// On ESP8266 it is fixed to GPIO3/RX0 and this setting has no effect
#define SPDIF_OUT_PIN 27
File dir;
AudioFileSourceSD *source = NULL;
AudioOutputSPDIF *output = NULL;
AudioGeneratorFLAC *decoder = NULL;
void setup() {
Serial.begin(115200);
Serial.println();
delay(1000);
audioLogger = &Serial;
source = new AudioFileSourceSD();
output = new AudioOutputSPDIF(SPDIF_OUT_PIN);
decoder = new AudioGeneratorFLAC();
// NOTE: SD.begin(...) should be called AFTER AudioOutputSPDIF()
// to takover the the SPI pins if they share some with I2S
// (i.e. D8 on Wemos D1 mini is both I2S BCK and SPI SS)
#if defined(ESP8266)
SD.begin(SS, SPI_SPEED);
#else
SD.begin();
#endif
dir = SD.open("/");
}
void loop() {
if ((decoder) && (decoder->isRunning())) {
if (!decoder->loop()) decoder->stop();
} else {
File file = dir.openNextFile();
if (file) {
if (String(file.name()).endsWith(".flac")) {
source->close();
if (source->open(file.name())) {
Serial.printf_P(PSTR("Playing '%s' from SD card...\n"), file.name());
decoder->begin(source, output);
} else {
Serial.printf_P(PSTR("Error opening '%s'\n"), file.name());
}
}
} else {
Serial.println(F("Playback form SD card done\n"));
delay(1000);
}
}
}

View File

@ -0,0 +1,33 @@
#include <Arduino.h>
#include <AudioOutputI2S.h>
#include <AudioFileSourcePROGMEM.h>
#include <AudioGeneratorFLAC.h>
#include "sample.h"
AudioOutputI2S *out;
AudioFileSourcePROGMEM *file;
AudioGeneratorFLAC *flac;
void setup()
{
Serial.begin(115200);
Serial.println("Starting up...\n");
audioLogger = &Serial;
file = new AudioFileSourcePROGMEM( sample_flac, sizeof(sample_flac) );
out = new AudioOutputI2S();
flac = new AudioGeneratorFLAC();
flac->begin(file, out);
}
void loop()
{
if (flac->isRunning()) {
if (!flac->loop()) flac->stop();
} else {
Serial.printf("FLAC done\n");
delay(1000);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,54 @@
#include <Arduino.h>
#ifdef ESP32
void setup() {
Serial.begin(115200);
Serial.printf("ERROR - ESP32 does not support LittleFS\n");
}
void loop() {}
#else
#include <ESP8266WiFi.h>
#include <AudioOutputI2S.h>
#include <AudioGeneratorMIDI.h>
#include <AudioFileSourceLittleFS.h>
AudioFileSourceLittleFS *sf2;
AudioFileSourceLittleFS *mid;
AudioOutputI2S *dac;
AudioGeneratorMIDI *midi;
void setup()
{
const char *soundfont = "/1mgm.sf2";
const char *midifile = "/furelise.mid";
WiFi.mode(WIFI_OFF);
Serial.begin(115200);
Serial.println("Starting up...\n");
audioLogger = &Serial;
sf2 = new AudioFileSourceLittleFS(soundfont);
mid = new AudioFileSourceLittleFS(midifile);
dac = new AudioOutputI2S();
midi = new AudioGeneratorMIDI();
midi->SetSoundfont(sf2);
midi->SetSampleRate(22050);
Serial.printf("BEGIN...\n");
midi->begin(mid, dac);
}
void loop()
{
if (midi->isRunning()) {
if (!midi->loop()) {
uint32_t e = millis();
midi->stop();
}
} else {
Serial.printf("MIDI done\n");
delay(1000);
}
}
#endif

Binary file not shown.

View File

@ -0,0 +1,55 @@
#include <Arduino.h>
#ifdef ESP32
#include <WiFi.h>
#include "SPIFFS.h"
#else
#include <ESP8266WiFi.h>
#endif
#include <AudioOutputNull.h>
#include <AudioOutputI2S.h>
#include <AudioGeneratorMIDI.h>
#include <AudioFileSourceSPIFFS.h>
AudioFileSourceSPIFFS *sf2;
AudioFileSourceSPIFFS *mid;
AudioOutputI2S *dac;
AudioGeneratorMIDI *midi;
void setup()
{
const char *soundfont = "/1mgm.sf2";
const char *midifile = "/furelise.mid";
WiFi.mode(WIFI_OFF);
Serial.begin(115200);
Serial.println("Starting up...\n");
audioLogger = &Serial;
sf2 = new AudioFileSourceSPIFFS(soundfont);
mid = new AudioFileSourceSPIFFS(midifile);
dac = new AudioOutputI2S();
midi = new AudioGeneratorMIDI();
midi->SetSoundfont(sf2);
midi->SetSampleRate(22050);
Serial.printf("BEGIN...\n");
midi->begin(mid, dac);
}
void loop()
{
if (midi->isRunning()) {
if (!midi->loop()) {
uint32_t e = millis();
midi->stop();
}
} else {
Serial.printf("MIDI done\n");
delay(1000);
}
}

Binary file not shown.

View File

@ -0,0 +1,44 @@
#include <Arduino.h>
#include "AudioFileSourcePROGMEM.h"
#include "AudioGeneratorMOD.h"
#include "AudioOutputI2S.h"
#ifdef ESP32
#include <WiFi.h>
#else
#include <ESP8266WiFi.h>
#endif
// enigma.mod sample from the mod archive: https://modarchive.org/index.php?request=view_by_moduleid&query=42146
#include "enigma.h"
AudioGeneratorMOD *mod;
AudioFileSourcePROGMEM *file;
AudioOutputI2S *out;
void setup()
{
WiFi.mode(WIFI_OFF); //WiFi.forceSleepBegin();
Serial.begin(115200);
delay(1000);
audioLogger = &Serial;
file = new AudioFileSourcePROGMEM( enigma_mod, sizeof(enigma_mod) );
// out = new AudioOutputI2S(0, 1); Uncomment this line, comment the next one to use the internal DAC channel 1 (pin25) on ESP32
out = new AudioOutputI2S();
mod = new AudioGeneratorMOD();
mod->SetBufferSize(3*1024);
mod->SetSampleRate(44100);
mod->SetStereoSeparation(32);
mod->begin(file, out);
}
void loop()
{
if (mod->isRunning()) {
if (!mod->loop()) mod->stop();
} else {
Serial.printf("MOD done\n");
delay(1000);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,72 @@
#include <Arduino.h>
#ifdef ESP32
#include <WiFi.h>
#include "SPIFFS.h"
#else
#include <ESP8266WiFi.h>
#endif
#include "AudioFileSourceSPIFFS.h"
#include "AudioFileSourceID3.h"
#include "AudioGeneratorMP3.h"
#include "AudioOutputI2SNoDAC.h"
// To run, set your ESP8266 build to 160MHz, and include a SPIFFS of 512KB or greater.
// Use the "Tools->ESP8266/ESP32 Sketch Data Upload" menu to write the MP3 to SPIFFS
// Then upload the sketch normally.
// pno_cs from https://ccrma.stanford.edu/~jos/pasp/Sound_Examples.html
AudioGeneratorMP3 *mp3;
AudioFileSourceSPIFFS *file;
AudioOutputI2SNoDAC *out;
AudioFileSourceID3 *id3;
// Called when a metadata event occurs (i.e. an ID3 tag, an ICY block, etc.
void MDCallback(void *cbData, const char *type, bool isUnicode, const char *string)
{
(void)cbData;
Serial.printf("ID3 callback for: %s = '", type);
if (isUnicode) {
string += 2;
}
while (*string) {
char a = *(string++);
if (isUnicode) {
string++;
}
Serial.printf("%c", a);
}
Serial.printf("'\n");
Serial.flush();
}
void setup()
{
WiFi.mode(WIFI_OFF);
Serial.begin(115200);
delay(1000);
SPIFFS.begin();
Serial.printf("Sample MP3 playback begins...\n");
audioLogger = &Serial;
file = new AudioFileSourceSPIFFS("/pno-cs.mp3");
id3 = new AudioFileSourceID3(file);
id3->RegisterMetadataCB(MDCallback, (void*)"ID3TAG");
out = new AudioOutputI2SNoDAC();
mp3 = new AudioGeneratorMP3();
mp3->begin(id3, out);
}
void loop()
{
if (mp3->isRunning()) {
if (!mp3->loop()) mp3->stop();
} else {
Serial.printf("MP3 done\n");
delay(1000);
}
}

Binary file not shown.

View File

@ -0,0 +1,99 @@
#include <Arduino.h>
#ifdef ESP32
#include "SPIFFS.h"
#endif
#include "AudioFileSourceSPIFFS.h"
#include "AudioFileSourceID3.h"
#include "AudioOutputSPDIF.h"
#include "AudioGeneratorMP3.h"
// To run, set your ESP8266 build to 160MHz, and include a SPIFFS partition
// big enough to hold your MP3 file. Find suitable MP3 file from i.e.
// https://docs.espressif.com/projects/esp-adf/en/latest/design-guide/audio-samples.html
// and download it into 'data' directory. Use the "Tools->ESP8266/ESP32 Sketch Data Upload"
// menu to write the MP3 to SPIFFS. Then upload the sketch normally.
AudioFileSourceSPIFFS *file;
AudioFileSourceID3 *id3;
AudioOutputSPDIF *out;
AudioGeneratorMP3 *mp3;
// Called when a metadata event occurs (i.e. an ID3 tag, an ICY block, etc.
void MDCallback(void *cbData, const char *type, bool isUnicode, const char *string)
{
(void)cbData;
Serial.printf("ID3 callback for: %s = '", type);
if (isUnicode) {
string += 2;
}
while (*string) {
char a = *(string++);
if (isUnicode) {
string++;
}
Serial.printf("%c", a);
}
Serial.printf("'\n");
Serial.flush();
}
void setup()
{
Serial.begin(115200);
delay(1000);
Serial.println();
audioLogger = &Serial;
SPIFFS.begin();
file = new AudioFileSourceSPIFFS();
id3 = NULL;
out = new AudioOutputSPDIF();
mp3 = new AudioGeneratorMP3();
String fileName = "";
// Find first MP3 file in SPIFF and play it
#ifdef ESP32
File dir, root = SPIFFS.open("/");
while ((dir = root.openNextFile())) {
if (String(dir.name()).endsWith(".mp3")) {
if (file->open(dir.name())) {
fileName = String(dir.name());
break;
}
}
dir = root.openNextFile();
}
#else
Dir dir = SPIFFS.openDir("");
while (dir.next()) {
if (dir.fileName().endsWith(".mp3")) {
if (file->open(dir.fileName().c_str())) {
fileName = dir.fileName();
break;
}
}
}
#endif
if (fileName.length() > 0) {
id3 = new AudioFileSourceID3(file);
id3->RegisterMetadataCB(MDCallback, (void*)"ID3TAG");
mp3->begin(id3, out);
Serial.printf("Playback of '%s' begins...\n", fileName.c_str());
} else {
Serial.println("Can't find .mp3 file in SPIFFS");
}
}
void loop()
{
if (mp3->isRunning()) {
if (!mp3->loop()) mp3->stop();
} else {
Serial.println("MP3 done");
delay(1000);
}
}

View File

@ -0,0 +1,41 @@
#include <Arduino.h>
#ifdef ESP32
#include <WiFi.h>
#include "SPIFFS.h"
#else
#include <ESP8266WiFi.h>
#endif
#include "AudioFileSourceSPIFFS.h"
#include "AudioGeneratorOpus.h"
#include "AudioOutputI2S.h"
// The includes OPUS file is from Kevin MacLeod (incompetech.com), Licensed under Creative Commons: By Attribution 3.0, http://creativecommons.org/licenses/by/3.0/
AudioGeneratorOpus *opus;
AudioFileSourceSPIFFS *file;
AudioOutputI2S *out;
void setup()
{
WiFi.mode(WIFI_OFF);
Serial.begin(115200);
delay(1000);
SPIFFS.begin();
Serial.printf("Sample Opus playback begins...\n");
audioLogger = &Serial;
file = new AudioFileSourceSPIFFS("/gs-16b-2c-44100hz.opus");
out = new AudioOutputI2S();
opus = new AudioGeneratorOpus();
opus->begin(file, out);
}
void loop()
{
if (opus->isRunning()) {
if (!opus->loop()) opus->stop();
} else {
Serial.printf("Opus done\n");
delay(1000);
}
}

View File

@ -0,0 +1,36 @@
#include "AudioFileSourcePROGMEM.h"
#include "AudioGeneratorRTTTL.h"
#include "AudioOutputI2S.h"
const char rudolph[] PROGMEM =
"Rudolph the Red Nosed Raindeer:d=8,o=5,b=250:g,4a,g,4e,4c6,4a,2g.,g,a,g,a,4g,4c6,2b.,4p,f,4g,f,4d,4b,4a,2g.,g,a,g,a,4g,4a,2e.,4p,g,4a,a,4e,4c6,4a,2g.,g,a,g,a,4g,4c6,2b.,4p,f,4g,f,4d,4b,4a,2g.,g,a,g,a,4g,4d6,2c.6,4p,4a,4a,4c6,4a,4g,4e,2g,4d,4e,4g,4a,4b,4b,2b,4c6,4c6,4b,4a,4g,4f,2d,g,4a,g,4e,4c6,4a,2g.,g,a,g,a,4g,4c6,2b.,4p,f,4g,f,4d,4b,4a,2g.,4g,4a,4g,4a,2g,2d6,1c.6.";
// Plenty more at: http://mines.lumpylumpy.com/Electronics/Computers/Software/Cpp/MFC/RingTones.RTTTL
AudioGeneratorRTTTL *rtttl;
AudioFileSourcePROGMEM *file;
AudioOutputI2S *out;
void setup()
{
Serial.begin(115200);
delay(1000);
Serial.printf("RTTTL start\n");
audioLogger = &Serial;
file = new AudioFileSourcePROGMEM( rudolph, strlen_P(rudolph) );
out = new AudioOutputI2S();
rtttl = new AudioGeneratorRTTTL();
rtttl->begin(file, out);
}
void loop()
{
if (rtttl->isRunning()) {
if (!rtttl->loop()) rtttl->stop();
} else {
Serial.printf("RTTTL done\n");
delay(1000);
}
}

View File

@ -0,0 +1,42 @@
#include <Arduino.h>
#ifdef ESP32
#include <WiFi.h>
#else
#include <ESP8266WiFi.h>
#endif
#include "AudioFileSourcePROGMEM.h"
#include "AudioGeneratorWAV.h"
#include "AudioOutputI2SNoDAC.h"
// VIOLA sample taken from https://ccrma.stanford.edu/~jos/pasp/Sound_Examples.html
#include "viola.h"
AudioGeneratorWAV *wav;
AudioFileSourcePROGMEM *file;
AudioOutputI2SNoDAC *out;
void setup()
{
WiFi.mode(WIFI_OFF);
Serial.begin(115200);
delay(1000);
Serial.printf("WAV start\n");
audioLogger = &Serial;
file = new AudioFileSourcePROGMEM( viola, sizeof(viola) );
out = new AudioOutputI2SNoDAC();
wav = new AudioGeneratorWAV();
wav->begin(file, out);
}
void loop()
{
if (wav->isRunning()) {
if (!wav->loop()) wav->stop();
} else {
Serial.printf("WAV done\n");
delay(1000);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,107 @@
#include <Arduino.h>
#ifdef ESP32
#include <WiFi.h>
#else
#include <ESP8266WiFi.h>
#endif
#include "AudioFileSourceICYStream.h"
#include "AudioFileSourceBuffer.h"
#include "AudioGeneratorMP3.h"
#include "AudioOutputI2SNoDAC.h"
// To run, set your ESP8266 build to 160MHz, update the SSID info, and upload.
// Enter your WiFi setup here:
#ifndef STASSID
#define STASSID "your-ssid"
#define STAPSK "your-password"
#endif
const char* ssid = STASSID;
const char* password = STAPSK;
// Randomly picked URL
const char *URL="http://streaming.shoutcast.com/80sPlanet?lang=en-US";
AudioGeneratorMP3 *mp3;
AudioFileSourceICYStream *file;
AudioFileSourceBuffer *buff;
AudioOutputI2SNoDAC *out;
// Called when a metadata event occurs (i.e. an ID3 tag, an ICY block, etc.
void MDCallback(void *cbData, const char *type, bool isUnicode, const char *string)
{
const char *ptr = reinterpret_cast<const char *>(cbData);
(void) isUnicode; // Punt this ball for now
// Note that the type and string may be in PROGMEM, so copy them to RAM for printf
char s1[32], s2[64];
strncpy_P(s1, type, sizeof(s1));
s1[sizeof(s1)-1]=0;
strncpy_P(s2, string, sizeof(s2));
s2[sizeof(s2)-1]=0;
Serial.printf("METADATA(%s) '%s' = '%s'\n", ptr, s1, s2);
Serial.flush();
}
// Called when there's a warning or error (like a buffer underflow or decode hiccup)
void StatusCallback(void *cbData, int code, const char *string)
{
const char *ptr = reinterpret_cast<const char *>(cbData);
// Note that the string may be in PROGMEM, so copy it to RAM for printf
char s1[64];
strncpy_P(s1, string, sizeof(s1));
s1[sizeof(s1)-1]=0;
Serial.printf("STATUS(%s) '%d' = '%s'\n", ptr, code, s1);
Serial.flush();
}
void setup()
{
Serial.begin(115200);
delay(1000);
Serial.println("Connecting to WiFi");
WiFi.disconnect();
WiFi.softAPdisconnect(true);
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
// Try forever
while (WiFi.status() != WL_CONNECTED) {
Serial.println("...Connecting to WiFi");
delay(1000);
}
Serial.println("Connected");
audioLogger = &Serial;
file = new AudioFileSourceICYStream(URL);
file->RegisterMetadataCB(MDCallback, (void*)"ICY");
buff = new AudioFileSourceBuffer(file, 2048);
buff->RegisterStatusCB(StatusCallback, (void*)"buffer");
out = new AudioOutputI2SNoDAC();
mp3 = new AudioGeneratorMP3();
mp3->RegisterStatusCB(StatusCallback, (void*)"mp3");
mp3->begin(buff, out);
}
void loop()
{
static int lastms = 0;
if (mp3->isRunning()) {
if (millis()-lastms > 1000) {
lastms = millis();
Serial.printf("Running for %d ms...\n", lastms);
Serial.flush();
}
if (!mp3->loop()) mp3->stop();
} else {
Serial.printf("MP3 done\n");
delay(1000);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -0,0 +1,104 @@
#include <Arduino.h>
#ifdef ESP32
#include <WiFi.h>
#else
#include <ESP8266WiFi.h>
#endif
#include "AudioFileSourceICYStream.h"
#include "AudioFileSourceSPIRAMBuffer.h"
#include "AudioGeneratorMP3.h"
#include "AudioOutputI2SNoDAC.h"
// To run, set your ESP8266 build to 160MHz, update the SSID info, and upload.
// Enter your WiFi setup here:
#ifndef STASSID
#define STASSID "your-ssid"
#define STAPSK "your-password"
#endif
const char* ssid = STASSID;
const char* password = STAPSK;
// Randomly picked URL
const char *URL="http://kvbstreams.dyndns.org:8000/wkvi-am";
AudioGeneratorMP3 *mp3;
AudioFileSourceICYStream *file;
AudioFileSourceSPIRAMBuffer *buff;
AudioOutputI2SNoDAC *out;
// Called when a metadata event occurs (i.e. an ID3 tag, an ICY block, etc.
void MDCallback(void *cbData, const char *type, bool isUnicode, const char *string)
{
const char *ptr = reinterpret_cast<const char *>(cbData);
(void) isUnicode; // Punt this ball for now
// Note that the type and string may be in PROGMEM, so copy them to RAM for printf
Serial.printf_P(PSTR("METADATA(%s) '%s' = '%s'\n"), ptr, type, string);
Serial.flush();
}
// Called when there's a warning or error (like a buffer underflow or decode hiccup)
void StatusCallback(void *cbData, int code, const char *string)
{
const char *ptr = reinterpret_cast<const char *>(cbData);
static uint32_t lastTime = 0;
static int lastCode = -99999;
uint32_t now = millis();
if ((lastCode != code) || (now - lastTime > 1000)) {
Serial.printf_P(PSTR("STATUS(%s) '%d' = '%s'\n"), ptr, code, string);
Serial.flush();
lastTime = now;
lastCode = code;
}
}
void setup()
{
Serial.begin(115200);
delay(1000);
Serial.println("Connecting to WiFi");
WiFi.disconnect();
WiFi.softAPdisconnect(true);
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
// Try forever
while (WiFi.status() != WL_CONNECTED) {
Serial.println("...Connecting to WiFi");
delay(1000);
}
Serial.println("Connected");
audioLogger = &Serial;
file = new AudioFileSourceICYStream(URL);
file->RegisterMetadataCB(MDCallback, (void*)"ICY");
// Initialize 23LC1024 SPI RAM buffer with chip select ion GPIO4 and ram size of 128KByte
buff = new AudioFileSourceSPIRAMBuffer(file, 4, 128*1024);
buff->RegisterStatusCB(StatusCallback, (void*)"buffer");
out = new AudioOutputI2SNoDAC();
mp3 = new AudioGeneratorMP3();
mp3->RegisterStatusCB(StatusCallback, (void*)"mp3");
mp3->begin(buff, out);
}
void loop()
{
static int lastms = 0;
if (mp3->isRunning()) {
if (millis()-lastms > 1000) {
lastms = millis();
Serial.printf("Running for %d ms...\n", lastms);
Serial.flush();
}
if (!mp3->loop()) mp3->stop();
} else {
Serial.printf("MP3 done\n");
delay(1000);
}
}

View File

@ -0,0 +1,185 @@
// Talking Clock example, with speech taken from
// https://github.com/going-digital/Talkie/blob/master/Talkie/examples/Vocab_US_Clock/Vocab_US_Clock.ino
// Released under GPL v2
#ifdef ESP32
#include <WiFi.h>
#else
#include <ESP8266WiFi.h>
#endif
#include <time.h>
#include "AudioFileSourcePROGMEM.h"
#include "AudioGeneratorTalkie.h"
#include "AudioOutputI2S.h"
#ifndef STASSID
#define STASSID "NOBABIES"
#define STAPSK "ElephantsAreGreat"
#endif
const char *ssid = STASSID;
const char *pass = STAPSK;
long timezone = 2;
byte daysavetime = 1;
uint8_t spTHE[] PROGMEM = {0x08,0xE8,0x3E,0x55,0x01,0xC3,0x86,0x27,0xAF,0x72,0x0D,0x4D,0x97,0xD5,0xBC,0x64,0x3C,0xF2,0x5C,0x51,0xF1,0x93,0x36,0x8F,0x4F,0x59,0x2A,0x42,0x7A,0x32,0xC3,0x64,0xFF,0x3F};
uint8_t spTIME[] PROGMEM = {0x0E,0x28,0xAC,0x2D,0x01,0x5D,0xB6,0x0D,0x33,0xF3,0x54,0xB3,0x60,0xBA,0x8C,0x54,0x5C,0xCD,0x2D,0xD4,0x32,0x73,0x0F,0x8E,0x34,0x33,0xCB,0x4A,0x25,0xD4,0x25,0x83,0x2C,0x2B,0xD5,0x50,0x97,0x08,0x32,0xEC,0xD4,0xDC,0x4C,0x33,0xC8,0x70,0x73,0x0F,0x33,0xCD,0x20,0xC3,0xCB,0x43,0xDD,0x3C,0xCD,0x8C,0x20,0x77,0x89,0xF4,0x94,0xB2,0xE2,0xE2,0x35,0x22,0x5D,0xD6,0x4A,0x8A,0x96,0xCC,0x36,0x25,0x2D,0xC9,0x9A,0x7B,0xC2,0x18,0x87,0x24,0x4B,0x1C,0xC9,0x50,0x19,0x92,0x2C,0x71,0x34,0x4B,0x45,0x8A,0x8B,0xC4,0x96,0xB6,0x5A,0x29,0x2A,0x92,0x5A,0xCA,0x53,0x96,0x20,0x05,0x09,0xF5,0x92,0x5D,0xBC,0xE8,0x58,0x4A,0xDD,0xAE,0x73,0xBD,0x65,0x4B,0x8D,0x78,0xCA,0x2B,0x4E,0xD8,0xD9,0xED,0x22,0x20,0x06,0x75,0x00,0x00,0x80,0xFF,0x07};
uint8_t spIS[] PROGMEM = {0x21,0x18,0x96,0x38,0xB7,0x14,0x8D,0x60,0x3A,0xA6,0xE8,0x51,0xB4,0xDC,0x2E,0x48,0x7B,0x5A,0xF1,0x70,0x1B,0xA3,0xEC,0x09,0xC6,0xCB,0xEB,0x92,0x3D,0xA7,0x69,0x1F,0xAF,0x71,0x89,0x9C,0xA2,0xB3,0xFC,0xCA,0x35,0x72,0x9A,0xD1,0xF0,0xAB,0x12,0xB3,0x2B,0xC6,0xCD,0x4F,0xCC,0x32,0x26,0x19,0x07,0xDF,0x0B,0x8F,0xB8,0xA4,0xED,0x7C,0xCF,0x23,0x62,0x8B,0x8E,0xF1,0x23,0x0A,0x8B,0x6E,0xCB,0xCE,0xEF,0x54,0x44,0x3C,0xDC,0x08,0x60,0x0B,0x37,0x01,0x1C,0x53,0x26,0x80,0x15,0x4E,0x14,0xB0,0x54,0x2B,0x02,0xA4,0x69,0xFF,0x7F};
uint8_t spA_M_[] PROGMEM = {0xCD,0xEF,0x86,0xAB,0x57,0x6D,0x0F,0xAF,0x71,0xAD,0x49,0x55,0x3C,0xFC,0x2E,0xC5,0xB7,0x5C,0xF1,0xF2,0x87,0x66,0xDD,0x4E,0xC5,0xC3,0xEF,0x92,0xE2,0x3A,0x65,0xB7,0xA0,0x09,0xAA,0x1B,0x97,0x54,0x82,0x2E,0x28,0x77,0x5C,0x52,0x09,0x1A,0xA3,0xB8,0x76,0x49,0x25,0x68,0x8C,0x73,0xDB,0x24,0x95,0xA0,0x32,0xA9,0x6B,0xA7,0xD9,0x82,0x26,0xA9,0x76,0x42,0xD6,0x08,0xBA,0xE1,0xE8,0x0E,0x5A,0x2B,0xEA,0x9E,0x3D,0x27,0x18,0xAD,0xA8,0x07,0xF1,0x98,0x90,0x35,0xA2,0x96,0x44,0xA3,0x5D,0x66,0x8B,0x6B,0x12,0xCD,0x32,0x85,0x25,0xC9,0x81,0x2D,0xC3,0x64,0x85,0x34,0x58,0x89,0x94,0x52,0x1C,0x52,0x2F,0x35,0xDA,0xC7,0x51,0x48,0x23,0x97,0xCC,0x2C,0x97,0x2E,0xF3,0x5C,0xF3,0xA2,0x14,0xBA,0x2C,0x48,0xCE,0xCA,0x76,0xE8,0x32,0x2F,0x34,0xB2,0xDB,0x85,0xC9,0x83,0x90,0xA8,0x2C,0x57,0x26,0x8F,0x9C,0xBD,0xA2,0x53,0xD9,0xC2,0x54,0x59,0x28,0x99,0x4B,0x2C,0x5D,0xFF,0x3F};
uint8_t spP_M_[] PROGMEM = {0x0E,0x98,0x41,0x54,0x00,0x43,0xA0,0x05,0xAB,0x42,0x8E,0x1D,0xA3,0x15,0xEC,0x4E,0x58,0xF7,0x92,0x66,0x70,0x1B,0x66,0xDB,0x73,0x99,0xC1,0xEB,0x98,0xED,0xD6,0x25,0x25,0x6F,0x70,0x92,0xDD,0x64,0xD8,0xFC,0x61,0xD0,0x66,0x83,0xD6,0x0A,0x86,0x23,0xAB,0x69,0xDA,0x2B,0x18,0x9E,0x3D,0x37,0x69,0x9D,0xA8,0x07,0x71,0x9F,0xA0,0xBD,0xA2,0x16,0xD5,0x7C,0x54,0xF6,0x88,0x6B,0x54,0x8B,0x34,0x49,0x2D,0x29,0x49,0x3C,0x34,0x64,0xA5,0x24,0x1B,0x36,0xD7,0x72,0x13,0x92,0xA4,0xC4,0x2D,0xC3,0xB3,0x4B,0xA3,0x62,0x0F,0x2B,0x37,0x6E,0x8B,0x5A,0xD4,0x3D,0xDD,0x9A,0x2D,0x50,0x93,0xF6,0x4C,0xAA,0xB6,0xC4,0x85,0x3B,0xB2,0xB1,0xD8,0x93,0x20,0x4D,0x8F,0x24,0xFF,0x0F};
uint8_t spOH[] PROGMEM = {0xC6,0xC9,0x71,0x5A,0xA2,0x92,0x14,0x2F,0x6E,0x97,0x9C,0x46,0x9D,0xDC,0xB0,0x4D,0x62,0x1B,0x55,0x70,0xDD,0x55,0xBE,0x0E,0x36,0xC1,0x33,0x37,0xA9,0xA7,0x51,0x1B,0xCF,0x3C,0xA5,0x9E,0x44,0xAC,0x3C,0x7D,0x98,0x7B,0x52,0x96,0x72,0x65,0x4B,0xF6,0x1A,0xD9,0xCA,0xF5,0x91,0x2D,0xA2,0x2A,0x4B,0xF7,0xFF,0x01};
uint8_t spOCLOCK[] PROGMEM = {0x21,0x4E,0x3D,0xB8,0x2B,0x19,0xBB,0x24,0x0E,0xE5,0xEC,0x60,0xE4,0xF2,0x90,0x13,0xD4,0x2A,0x11,0x80,0x00,0x42,0x69,0x26,0x40,0xD0,0x2B,0x04,0x68,0xE0,0x4D,0x00,0x3A,0x35,0x35,0x33,0xB6,0x51,0xD9,0x64,0x34,0x82,0xB4,0x9A,0x63,0x92,0x55,0x89,0x52,0x5B,0xCA,0x2E,0x34,0x25,0x4E,0x63,0x28,0x3A,0x50,0x95,0x26,0x8D,0xE6,0xAA,0x64,0x58,0xEA,0x92,0xCE,0xC2,0x46,0x15,0x9B,0x86,0xCD,0x2A,0x2E,0x37,0x00,0x00,0x00,0x0C,0xC8,0xDD,0x05,0x01,0xB9,0x33,0x21,0xA0,0x74,0xD7,0xFF,0x07};
uint8_t spONE[] PROGMEM = {0xCC,0x67,0x75,0x42,0x59,0x5D,0x3A,0x4F,0x9D,0x36,0x63,0xB7,0x59,0xDC,0x30,0x5B,0x5C,0x23,0x61,0xF3,0xE2,0x1C,0xF1,0xF0,0x98,0xC3,0x4B,0x7D,0x39,0xCA,0x1D,0x2C,0x2F,0xB7,0x15,0xEF,0x70,0x79,0xBC,0xD2,0x46,0x7C,0x52,0xE5,0xF1,0x4A,0x6A,0xB3,0x71,0x47,0xC3,0x2D,0x39,0x34,0x4B,0x23,0x35,0xB7,0x7A,0x55,0x33,0x8F,0x59,0xDC,0xA2,0x44,0xB5,0xBC,0x66,0x72,0x8B,0x64,0xF5,0xF6,0x98,0xC1,0x4D,0x42,0xD4,0x27,0x62,0x38,0x2F,0x4A,0xB6,0x9C,0x88,0x68,0xBC,0xA6,0x95,0xF8,0x5C,0xA1,0x09,0x86,0x77,0x91,0x11,0x5B,0xFF,0x0F};
uint8_t spTWO[] PROGMEM = {0x0E,0x38,0x6E,0x25,0x00,0xA3,0x0D,0x3A,0xA0,0x37,0xC5,0xA0,0x05,0x9E,0x56,0x35,0x86,0xAA,0x5E,0x8C,0xA4,0x82,0xB2,0xD7,0x74,0x31,0x22,0x69,0xAD,0x1C,0xD3,0xC1,0xD0,0xFA,0x28,0x2B,0x2D,0x47,0xC3,0x1B,0xC2,0xC4,0xAE,0xC6,0xCD,0x9C,0x48,0x53,0x9A,0xFF,0x0F};
uint8_t spTHREE[] PROGMEM = {0x02,0xD8,0x2E,0x9C,0x01,0xDB,0xA6,0x33,0x60,0xFB,0x30,0x01,0xEC,0x20,0x12,0x8C,0xE4,0xD8,0xCA,0x32,0x96,0x73,0x63,0x41,0x39,0x89,0x98,0xC1,0x4D,0x0D,0xED,0xB0,0x2A,0x05,0x37,0x0F,0xB4,0xA5,0xAE,0x5C,0xDC,0x36,0xD0,0x83,0x2F,0x4A,0x71,0x7B,0x03,0xF7,0x38,0x59,0xCD,0xED,0x1E,0xB4,0x6B,0x14,0x35,0xB7,0x6B,0x94,0x99,0x91,0xD5,0xDC,0x26,0x48,0x77,0x4B,0x66,0x71,0x1B,0x21,0xDB,0x2D,0x8A,0xC9,0x6D,0x88,0xFC,0x26,0x28,0x3A,0xB7,0x21,0xF4,0x1F,0xA3,0x65,0xBC,0x02,0x38,0xBB,0x3D,0x8E,0xF0,0x2B,0xE2,0x08,0xB7,0x34,0xFF,0x0F};
uint8_t spFOUR[] PROGMEM = {0x0C,0x18,0xB6,0x9A,0x01,0xC3,0x75,0x09,0x60,0xD8,0x0E,0x09,0x30,0xA0,0x9B,0xB6,0xA0,0xBB,0xB0,0xAA,0x16,0x4E,0x82,0xEB,0xEA,0xA9,0xFA,0x59,0x49,0x9E,0x59,0x23,0x9A,0x27,0x3B,0x78,0x66,0xAE,0x4A,0x9C,0x9C,0xE0,0x99,0xD3,0x2A,0xBD,0x72,0x92,0xEF,0xE6,0x88,0xE4,0x45,0x4D,0x7E,0x98,0x2D,0x62,0x67,0x37,0xF9,0xA1,0x37,0xA7,0x6C,0x94,0xE4,0xC7,0x1E,0xDC,0x3C,0xA5,0x83,0x1F,0x8B,0xEB,0x52,0x0E,0x0E,0x7E,0x2E,0x4E,0xC7,0x31,0xD2,0x79,0xA5,0x3A,0x0D,0xD9,0xC4,0xFF,0x07};
uint8_t spFIVE[] PROGMEM = {0x02,0xE8,0x3E,0x8C,0x01,0xDD,0x65,0x08,0x60,0x98,0x4C,0x06,0x34,0x93,0xCE,0x80,0xE6,0xDA,0x9A,0x14,0x6B,0xAA,0x47,0xD1,0x5E,0x56,0xAA,0x6D,0x56,0xCD,0x78,0xD9,0xA9,0x1C,0x67,0x05,0x83,0xE1,0xA4,0xBA,0x38,0xEE,0x16,0x86,0x9B,0xFA,0x60,0x87,0x5B,0x18,0x6E,0xEE,0x8B,0x1D,0x6E,0x61,0xB9,0x69,0x36,0x65,0xBA,0x8D,0xE5,0xE5,0x3E,0x1C,0xE9,0x0E,0x96,0x9B,0x5B,0xAB,0x95,0x2B,0x58,0x6E,0xCE,0xE5,0x3A,0x6A,0xF3,0xB8,0x35,0x84,0x7B,0x05,0xA3,0xE3,0x36,0xEF,0x92,0x19,0xB4,0x86,0xDB,0xB4,0x69,0xB4,0xD1,0x2A,0x4E,0x65,0x9A,0x99,0xCE,0x28,0xD9,0x85,0x71,0x4C,0x18,0x6D,0x67,0x47,0xC6,0x5E,0x53,0x4A,0x9C,0xB5,0xE2,0x85,0x45,0x26,0xFE,0x7F};
uint8_t spSIX[] PROGMEM = {0x0E,0xD8,0xAE,0xDD,0x03,0x0E,0x38,0xA6,0xD2,0x01,0xD3,0xB4,0x2C,0xAD,0x6A,0x35,0x9D,0xB1,0x7D,0xDC,0xEE,0xC4,0x65,0xD7,0xF1,0x72,0x47,0x24,0xB3,0x19,0xD9,0xD9,0x05,0x70,0x40,0x49,0xEA,0x02,0x98,0xBE,0x42,0x01,0xDF,0xA4,0x69,0x40,0x00,0xDF,0x95,0xFC,0x3F};
uint8_t spSEVEN[] PROGMEM = {0x02,0xB8,0x3A,0x8C,0x01,0xDF,0xA4,0x73,0x40,0x01,0x47,0xB9,0x2F,0x33,0x3B,0x73,0x5F,0x53,0x7C,0xEC,0x9A,0xC5,0x63,0xD5,0xD1,0x75,0xAE,0x5B,0xFC,0x64,0x5C,0x35,0x87,0x91,0xF1,0x83,0x36,0xB5,0x68,0x55,0xC5,0x6F,0xDA,0x45,0x2D,0x1C,0x2D,0xB7,0x38,0x37,0x9F,0x60,0x3C,0xBC,0x9A,0x85,0xA3,0x25,0x66,0xF7,0x8A,0x57,0x1C,0xA9,0x67,0x56,0xCA,0x5E,0xF0,0xB2,0x16,0xB2,0xF1,0x89,0xCE,0x8B,0x92,0x25,0xC7,0x2B,0x33,0xCF,0x48,0xB1,0x99,0xB4,0xF3,0xFF};
uint8_t spEIGHT[] PROGMEM = {0xC3,0x6C,0x86,0xB3,0x27,0x6D,0x0F,0xA7,0x48,0x99,0x4E,0x55,0x3C,0xBC,0x22,0x65,0x36,0x4D,0xD1,0xF0,0x32,0xD3,0xBE,0x34,0xDA,0xC3,0xEB,0x82,0xE2,0xDA,0x65,0x35,0xAF,0x31,0xF2,0x6B,0x97,0x95,0xBC,0x86,0xD8,0x6F,0x82,0xA6,0x73,0x0B,0xC6,0x9E,0x72,0x99,0xCC,0xCB,0x02,0xAD,0x3C,0x9A,0x10,0x60,0xAB,0x62,0x05,0x2C,0x37,0x84,0x00,0xA9,0x73,0x00,0x00,0xFE,0x1F};
uint8_t spNINE[] PROGMEM = {0xCC,0xA1,0x26,0xBB,0x83,0x93,0x18,0xCF,0x4A,0xAD,0x2E,0x31,0xED,0x3C,0xA7,0x24,0x26,0xC3,0x54,0xF1,0x92,0x64,0x8B,0x8A,0x98,0xCB,0x2B,0x2E,0x34,0x53,0x2D,0x0E,0x2F,0x57,0xB3,0x0C,0x0D,0x3C,0xBC,0x3C,0x4C,0x4B,0xCA,0xF4,0xF0,0x72,0x0F,0x6E,0x49,0x53,0xCD,0xCB,0x53,0x2D,0x35,0x4D,0x0F,0x2F,0x0F,0xD7,0x0C,0x0D,0x3D,0xBC,0xDC,0x4D,0xD3,0xDD,0xC2,0xF0,0x72,0x52,0x4F,0x57,0x9B,0xC3,0xAB,0x89,0xBD,0x42,0x2D,0x0F,0xAF,0x5A,0xD1,0x71,0x91,0x55,0xBC,0x2C,0xC5,0x3B,0xD8,0x65,0xF2,0x82,0x94,0x18,0x4E,0x3B,0xC1,0x73,0x42,0x32,0x33,0x15,0x45,0x4F,0x79,0x52,0x6A,0x55,0xA6,0xA3,0xFF,0x07};
uint8_t spTEN[] PROGMEM = {0x0E,0xD8,0xB1,0xDD,0x01,0x3D,0xA8,0x24,0x7B,0x04,0x27,0x76,0x77,0xDC,0xEC,0xC2,0xC5,0x23,0x84,0xCD,0x72,0x9A,0x51,0xF7,0x62,0x45,0xC7,0xEB,0x4E,0x35,0x4A,0x14,0x2D,0xBF,0x45,0xB6,0x0A,0x75,0xB8,0xFC,0x16,0xD9,0x2A,0xD9,0xD6,0x0A,0x5A,0x10,0xCD,0xA2,0x48,0x23,0xA8,0x81,0x35,0x4B,0x2C,0xA7,0x20,0x69,0x0A,0xAF,0xB6,0x15,0x82,0xA4,0x29,0x3C,0xC7,0x52,0x08,0xA2,0x22,0xCF,0x68,0x4B,0x2E,0xF0,0x8A,0xBD,0xA3,0x2C,0xAB,0x40,0x1B,0xCE,0xAA,0xB2,0x6C,0x82,0x40,0x4D,0x7D,0xC2,0x89,0x88,0x8A,0x61,0xCC,0x74,0xD5,0xFF,0x0F};
uint8_t spELEVEN[] PROGMEM = {0xC3,0xCD,0x76,0x5C,0xAE,0x14,0x0F,0x37,0x9B,0x71,0xDE,0x92,0x55,0xBC,0x2C,0x27,0x70,0xD3,0x76,0xF0,0x83,0x5E,0xA3,0x5E,0x5A,0xC1,0xF7,0x61,0x58,0xA7,0x19,0x35,0x3F,0x99,0x31,0xDE,0x52,0x74,0xFC,0xA2,0x26,0x64,0x4B,0xD1,0xF1,0xAB,0xAE,0xD0,0x2D,0xC5,0xC7,0x2F,0x36,0xDD,0x27,0x15,0x0F,0x3F,0xD9,0x08,0x9F,0x62,0xE4,0xC2,0x2C,0xD4,0xD8,0xD3,0x89,0x0B,0x1B,0x57,0x11,0x0B,0x3B,0xC5,0xCF,0xD6,0xCC,0xC6,0x64,0x35,0xAF,0x18,0x73,0x1F,0xA1,0x5D,0xBC,0x62,0x45,0xB3,0x45,0x51,0xF0,0xA2,0x62,0xAB,0x4A,0x5B,0xC9,0x4B,0x8A,0x2D,0xB3,0x6C,0x06,0x2F,0x29,0xB2,0xAC,0x8A,0x18,0xBC,0x28,0xD9,0xAA,0xD2,0x92,0xF1,0xBC,0xE0,0x98,0x8C,0x48,0xCC,0x17,0x52,0xA3,0x27,0x6D,0x93,0xD0,0x4B,0x8E,0x0E,0x77,0x02,0x00,0xFF,0x0F};
uint8_t spTWELVE[] PROGMEM = {0x06,0x28,0x46,0xD3,0x01,0x25,0x06,0x13,0x20,0xBA,0x70,0x70,0xB6,0x79,0xCA,0x36,0xAE,0x28,0x38,0xE1,0x29,0xC5,0x35,0xA3,0xE6,0xC4,0x16,0x6A,0x53,0x8C,0x97,0x9B,0x72,0x86,0x4F,0x28,0x1A,0x6E,0x0A,0x59,0x36,0xAE,0x68,0xF8,0x29,0x67,0xFA,0x06,0xA3,0x16,0xC4,0x96,0xE6,0x53,0xAC,0x5A,0x9C,0x56,0x72,0x77,0x31,0x4E,0x49,0x5C,0x8D,0x5B,0x29,0x3B,0x24,0x61,0x1E,0x6C,0x9B,0x6C,0x97,0xF8,0xA7,0x34,0x19,0x92,0x4C,0x62,0x9E,0x72,0x65,0x58,0x12,0xB1,0x7E,0x09,0xD5,0x2E,0x53,0xC5,0xBA,0x36,0x6B,0xB9,0x2D,0x17,0x05,0xEE,0x9A,0x6E,0x8E,0x05,0x50,0x6C,0x19,0x07,0x18,0x50,0xBD,0x3B,0x01,0x92,0x08,0x41,0x40,0x10,0xA6,0xFF,0x0F};
uint8_t spTHIRTEEN[] PROGMEM = {0x08,0xE8,0x2C,0x15,0x01,0x43,0x07,0x13,0xE0,0x98,0xB4,0xA6,0x35,0xA9,0x1E,0xDE,0x56,0x8E,0x53,0x9C,0x7A,0xE7,0xCA,0x5E,0x76,0x8D,0x94,0xE5,0x2B,0xAB,0xD9,0xB5,0x62,0xA4,0x9C,0xE4,0xE6,0xB4,0x41,0x1E,0x7C,0xB6,0x93,0xD7,0x16,0x99,0x5A,0xCD,0x61,0x76,0x55,0xC2,0x91,0x61,0x1B,0xC0,0x01,0x5D,0x85,0x05,0xE0,0x68,0x51,0x07,0x1C,0xA9,0x64,0x80,0x1D,0x4C,0x9C,0x95,0x88,0xD4,0x04,0x3B,0x4D,0x4E,0x21,0x5C,0x93,0xA8,0x26,0xB9,0x05,0x4B,0x6E,0xA0,0xE2,0xE4,0x57,0xC2,0xB9,0xC1,0xB2,0x93,0x5F,0x09,0xD7,0x24,0xCB,0x4E,0x41,0x25,0x54,0x1D,0x62,0x3B,0x05,0x8D,0x52,0x57,0xAA,0xAD,0x10,0x24,0x26,0xE3,0xE1,0x36,0x5D,0x10,0x85,0xB4,0x97,0x85,0x72,0x41,0x14,0x52,0x5E,0x1A,0xCA,0xF9,0x91,0x6B,0x7A,0x5B,0xC4,0xE0,0x17,0x2D,0x54,0x1D,0x92,0x8C,0x1F,0x25,0x4B,0x8F,0xB2,0x16,0x41,0xA1,0x4A,0x3E,0xE6,0xFA,0xFF,0x01};
uint8_t spFOURTEEN[] PROGMEM = {0x0C,0x58,0xAE,0x5C,0x01,0xD9,0x87,0x07,0x51,0xB7,0x25,0xB3,0x8A,0x15,0x2C,0xF7,0x1C,0x35,0x87,0x4D,0xB2,0xDD,0x53,0xCE,0x28,0x2B,0xC9,0x0E,0x97,0x2D,0xBD,0x2A,0x17,0x27,0x76,0x8E,0xD2,0x9A,0x6C,0x80,0x94,0x71,0x00,0x00,0x02,0xB0,0x58,0x58,0x00,0x9E,0x0B,0x0A,0xC0,0xB2,0xCE,0xC1,0xC8,0x98,0x7A,0x52,0x95,0x24,0x2B,0x11,0xED,0x36,0xD4,0x92,0xDC,0x4C,0xB5,0xC7,0xC8,0x53,0xF1,0x2A,0xE5,0x1A,0x17,0x55,0xC5,0xAF,0x94,0xBB,0xCD,0x1C,0x26,0xBF,0x52,0x9A,0x72,0x53,0x98,0xFC,0xC2,0x68,0xD2,0x4D,0x61,0xF0,0xA3,0x90,0xB6,0xD6,0x50,0xC1,0x8F,0x42,0xDA,0x4A,0x43,0x39,0x3F,0x48,0x2D,0x6B,0x33,0xF9,0xFF};
uint8_t spFIFTEEN[] PROGMEM = {0x08,0xE8,0x2A,0x0D,0x01,0xDD,0xBA,0x31,0x60,0x6A,0xF7,0xA0,0xAE,0x54,0xAA,0x5A,0x76,0x97,0xD9,0x34,0x69,0xEF,0x32,0x1E,0x66,0xE1,0xE2,0xB3,0x43,0xA9,0x18,0x55,0x92,0x4E,0x37,0x2D,0x67,0x6F,0xDF,0xA2,0x5A,0xB6,0x04,0x30,0x55,0xA8,0x00,0x86,0x09,0xE7,0x00,0x01,0x16,0x17,0x05,0x70,0x40,0x57,0xE5,0x01,0xF8,0x21,0x34,0x00,0xD3,0x19,0x33,0x80,0x89,0x9A,0x62,0x34,0x4C,0xD5,0x49,0xAE,0x8B,0x53,0x09,0xF7,0x26,0xD9,0x6A,0x7E,0x23,0x5C,0x13,0x12,0xB3,0x04,0x9D,0x50,0x4F,0xB1,0xAD,0x14,0x15,0xC2,0xD3,0xA1,0xB6,0x42,0x94,0xA8,0x8C,0x87,0xDB,0x74,0xB1,0x70,0x59,0xE1,0x2E,0xC9,0xC5,0x81,0x5B,0x55,0xA4,0x4C,0x17,0x47,0xC1,0x6D,0xE3,0x81,0x53,0x9C,0x84,0x6A,0x46,0xD9,0x4C,0x51,0x31,0x42,0xD9,0x66,0xC9,0x44,0x85,0x29,0x6A,0x9B,0xAD,0xFF,0x07};
uint8_t spSIXTEEN[] PROGMEM = {0x0A,0x58,0x5A,0x5D,0x00,0x93,0x97,0x0B,0x60,0xA9,0x48,0x05,0x0C,0x15,0xAE,0x80,0xAD,0x3D,0x14,0x30,0x7D,0xD9,0x50,0x92,0x92,0xAC,0x0D,0xC5,0xCD,0x2A,0x82,0xAA,0x3B,0x98,0x04,0xB3,0x4A,0xC8,0x9A,0x90,0x05,0x09,0x68,0x51,0xD4,0x01,0x23,0x9F,0x1A,0x60,0xA9,0x12,0x03,0xDC,0x50,0x81,0x80,0x22,0xDC,0x20,0x00,0xCB,0x06,0x3A,0x60,0x16,0xE3,0x64,0x64,0x42,0xDD,0xCD,0x6A,0x8A,0x5D,0x28,0x75,0x07,0xA9,0x2A,0x5E,0x65,0x34,0xED,0x64,0xBB,0xF8,0x85,0xF2,0x94,0x8B,0xAD,0xE4,0x37,0x4A,0x5B,0x21,0xB6,0x52,0x50,0x19,0xAD,0xA7,0xD8,0x4A,0x41,0x14,0xDA,0x5E,0x12,0x3A,0x04,0x91,0x4B,0x7B,0x69,0xA8,0x10,0x24,0x2E,0xE5,0xA3,0x81,0x52,0x90,0x94,0x5A,0x55,0x98,0x32,0x41,0x50,0xCC,0x93,0x2E,0x47,0x85,0x89,0x1B,0x5B,0x5A,0x62,0x04,0x44,0xE3,0x02,0x80,0x80,0x64,0xDD,0xFF,0x1F};
uint8_t spSEVENTEEN[] PROGMEM = {0x02,0x98,0x3A,0x42,0x00,0x5B,0xA6,0x09,0x60,0xDB,0x52,0x06,0x1C,0x93,0x29,0x80,0xA9,0x52,0x87,0x9A,0xB5,0x99,0x4F,0xC8,0x3E,0x46,0xD6,0x5E,0x7E,0x66,0xFB,0x98,0xC5,0x5A,0xC6,0x9A,0x9C,0x63,0x15,0x6B,0x11,0x13,0x8A,0x9C,0x97,0xB9,0x9A,0x5A,0x39,0x71,0xEE,0xD2,0x29,0xC2,0xA6,0xB8,0x58,0x59,0x99,0x56,0x14,0xA3,0xE1,0x26,0x19,0x19,0xE3,0x8C,0x93,0x17,0xB4,0x46,0xB5,0x88,0x71,0x9E,0x97,0x9E,0xB1,0x2C,0xC5,0xF8,0x56,0xC4,0x58,0xA3,0x1C,0xE1,0x33,0x9D,0x13,0x41,0x8A,0x43,0x58,0xAD,0x95,0xA9,0xDB,0x36,0xC0,0xD1,0xC9,0x0E,0x58,0x4E,0x45,0x01,0x23,0xA9,0x04,0x37,0x13,0xAE,0x4D,0x65,0x52,0x82,0xCA,0xA9,0x37,0x99,0x4D,0x89,0xBA,0xC0,0xBC,0x14,0x36,0x25,0xEA,0x1C,0x73,0x52,0x1D,0x97,0xB8,0x33,0xAC,0x0E,0x75,0x9C,0xE2,0xCE,0xB0,0xDA,0xC3,0x51,0x4A,0x1A,0xA5,0xCA,0x70,0x5B,0x21,0xCE,0x4C,0x26,0xD2,0x6C,0xBA,0x38,0x71,0x2E,0x1F,0x2D,0xED,0xE2,0x24,0xB8,0xBC,0x3D,0x52,0x88,0xAB,0x50,0x8E,0xA8,0x48,0x22,0x4E,0x42,0xA0,0x26,0x55,0xFD,0x3F};
uint8_t spEIGHTEEN[] PROGMEM = {0x2E,0x9C,0xD1,0x4D,0x54,0xEC,0x2C,0xBF,0x1B,0x8A,0x99,0x70,0x7C,0xFC,0x2E,0x29,0x6F,0x52,0xF6,0xF1,0xBA,0x20,0xBF,0x36,0xD9,0xCD,0xED,0x0C,0xF3,0x27,0x64,0x17,0x73,0x2B,0xA2,0x99,0x90,0x65,0xEC,0xED,0x40,0x73,0x32,0x12,0xB1,0xAF,0x30,0x35,0x0B,0xC7,0x00,0xE0,0x80,0xAE,0xDD,0x1C,0x70,0x43,0xAA,0x03,0x86,0x51,0x36,0xC0,0x30,0x64,0xCE,0x4C,0x98,0xFB,0x5C,0x65,0x07,0xAF,0x10,0xEA,0x0B,0x66,0x1B,0xFC,0x46,0xA8,0x3E,0x09,0x4D,0x08,0x2A,0xA6,0x3E,0x67,0x36,0x21,0x2A,0x98,0x67,0x9D,0x15,0xA7,0xA8,0x60,0xEE,0xB6,0x94,0x99,0xA2,0x4A,0x78,0x22,0xC2,0xA6,0x8B,0x8C,0x8E,0xCC,0x4C,0x8A,0x2E,0x8A,0x4C,0xD3,0x57,0x03,0x87,0x28,0x71,0x09,0x1F,0x2B,0xE4,0xA2,0xC4,0xC5,0x6D,0xAD,0x54,0x88,0xB2,0x63,0xC9,0xF2,0x50,0x2E,0x8A,0x4A,0x38,0x4A,0xEC,0x88,0x28,0x08,0xE3,0x28,0x49,0xF3,0xFF};
uint8_t spNINETEEN[] PROGMEM = {0xC2,0xEA,0x8A,0x95,0x2B,0x6A,0x05,0x3F,0x71,0x71,0x5F,0x0D,0x12,0xFC,0x28,0x25,0x62,0x35,0xF0,0xF0,0xB3,0x48,0x1E,0x0F,0xC9,0xCB,0x2F,0x45,0x7C,0x2C,0x25,0x1F,0xBF,0x14,0xB3,0x2C,0xB5,0x75,0xFC,0x5A,0x5C,0xA3,0x5D,0xE1,0xF1,0x7A,0x76,0xB3,0x4E,0x45,0xC7,0xED,0x96,0x23,0x3B,0x18,0x37,0x7B,0x18,0xCC,0x09,0x51,0x13,0x4C,0xAB,0x6C,0x4C,0x4B,0x96,0xD2,0x49,0xAA,0x36,0x0B,0xC5,0xC2,0x20,0x26,0x27,0x35,0x63,0x09,0x3D,0x30,0x8B,0xF0,0x48,0x5C,0xCA,0x61,0xDD,0xCB,0xCD,0x91,0x03,0x8E,0x4B,0x76,0xC0,0xCC,0x4D,0x06,0x98,0x31,0x31,0x98,0x99,0x70,0x6D,0x2A,0xA3,0xE4,0x16,0xCA,0xBD,0xCE,0x5C,0x92,0x57,0x28,0xCF,0x09,0x69,0x2E,0x7E,0xA5,0x3C,0x63,0xA2,0x30,0x05,0x95,0xD2,0x74,0x98,0xCD,0x14,0x54,0xCA,0x53,0xA9,0x96,0x52,0x50,0x28,0x6F,0xBA,0xCB,0x0C,0x41,0x50,0xDE,0x65,0x2E,0xD3,0x05,0x89,0x4B,0x7B,0x6B,0x20,0x17,0x44,0xAE,0xED,0x23,0x81,0x52,0x90,0x85,0x73,0x57,0xD0,0x72,0x41,0xB1,0x02,0xDE,0x2E,0xDB,0x04,0x89,0x05,0x79,0xBB,0x62,0xE5,0x76,0x11,0xCA,0x61,0x0E,0xFF,0x1F};
uint8_t spTWENTY[] PROGMEM = {0x01,0x98,0xD1,0xC2,0x00,0xCD,0xA4,0x32,0x20,0x79,0x13,0x04,0x28,0xE7,0x92,0xDC,0x70,0xCC,0x5D,0xDB,0x76,0xF3,0xD2,0x32,0x0B,0x0B,0x5B,0xC3,0x2B,0xCD,0xD4,0xDD,0x23,0x35,0xAF,0x44,0xE1,0xF0,0xB0,0x6D,0x3C,0xA9,0xAD,0x3D,0x35,0x0E,0xF1,0x0C,0x8B,0x28,0xF7,0x34,0x01,0x68,0x22,0xCD,0x00,0xC7,0xA4,0x04,0xBB,0x32,0xD6,0xAC,0x56,0x9C,0xDC,0xCA,0x28,0x66,0x53,0x51,0x70,0x2B,0xA5,0xBC,0x0D,0x9A,0xC1,0xEB,0x14,0x73,0x37,0x29,0x19,0xAF,0x33,0x8C,0x3B,0xA7,0x24,0xBC,0x42,0xB0,0xB7,0x59,0x09,0x09,0x3C,0x96,0xE9,0xF4,0x58,0xFF,0x0F};
uint8_t spTHIRTY[] PROGMEM = {0x08,0x98,0xD6,0x15,0x01,0x43,0xBB,0x0A,0x20,0x1B,0x8B,0xE5,0x16,0xA3,0x1E,0xB6,0xB6,0x96,0x97,0x3C,0x57,0xD4,0x2A,0x5E,0x7E,0x4E,0xD8,0xE1,0x6B,0x7B,0xF8,0x39,0x63,0x0D,0x9F,0x95,0xE1,0xE7,0x4C,0x76,0xBC,0x91,0x5B,0x90,0x13,0xC6,0x68,0x57,0x4E,0x41,0x8B,0x10,0x5E,0x1D,0xA9,0x44,0xD3,0xBA,0x47,0xB8,0xDD,0xE4,0x35,0x86,0x11,0x93,0x94,0x92,0x5F,0x29,0xC7,0x4C,0x30,0x0C,0x41,0xC5,0x1C,0x3B,0x2E,0xD3,0x05,0x15,0x53,0x6C,0x07,0x4D,0x15,0x14,0x8C,0xB5,0xC9,0x6A,0x44,0x90,0x10,0x4E,0x9A,0xB6,0x21,0x81,0x23,0x3A,0x91,0x91,0xE8,0xFF,0x01};
uint8_t spFOURTY[] PROGMEM = {0x04,0x18,0xB6,0x4C,0x00,0xC3,0x56,0x30,0xA0,0xE8,0xF4,0xA0,0x98,0x99,0x62,0x91,0xAE,0x83,0x6B,0x77,0x89,0x78,0x3B,0x09,0xAE,0xBD,0xA6,0x1E,0x63,0x3B,0x79,0x7E,0x71,0x5A,0x8F,0x95,0xE6,0xA5,0x4A,0x69,0xB9,0x4E,0x8A,0x5F,0x12,0x56,0xE4,0x58,0x69,0xE1,0x36,0xA1,0x69,0x2E,0x2B,0xF9,0x95,0x93,0x55,0x17,0xED,0xE4,0x37,0xC6,0xBA,0x93,0xB2,0x92,0xDF,0x19,0xD9,0x6E,0xC8,0x0A,0xFE,0x60,0xE8,0x37,0x21,0xC9,0xF9,0x8D,0x61,0x5F,0x32,0x13,0xE7,0x17,0x4C,0xD3,0xC6,0xB1,0x94,0x97,0x10,0x8F,0x8B,0xAD,0x11,0x7E,0xA1,0x9A,0x26,0x92,0xF6,0xFF,0x01};
uint8_t spFIFTY[] PROGMEM = {0x08,0xE8,0x2E,0x84,0x00,0x23,0x84,0x13,0x60,0x38,0x95,0xA5,0x0F,0xCF,0xE2,0x79,0x8A,0x8F,0x37,0x02,0xB3,0xD5,0x2A,0x6E,0x5E,0x93,0x94,0x79,0x45,0xD9,0x05,0x5D,0x0A,0xB9,0x97,0x63,0x02,0x74,0xA7,0x82,0x80,0xEE,0xC3,0x10,0xD0,0x7D,0x28,0x03,0x6E,0x14,0x06,0x70,0xE6,0x0A,0xC9,0x9A,0x4E,0x37,0xD9,0x95,0x51,0xCE,0xBA,0xA2,0x14,0x0C,0x81,0x36,0x1B,0xB2,0x5C,0x30,0x38,0xFA,0x9C,0xC9,0x32,0x41,0xA7,0x18,0x3B,0xA2,0x48,0x04,0x05,0x51,0x4F,0x91,0x6D,0x12,0x04,0x20,0x9B,0x61,0x89,0xFF,0x1F};
uint8_t spGOOD[] PROGMEM = {0x0A,0x28,0xCD,0x34,0x20,0xD9,0x1A,0x45,0x74,0xE4,0x66,0x24,0xAD,0xBA,0xB1,0x8C,0x9B,0x91,0xA5,0x64,0xE6,0x98,0x21,0x16,0x0B,0x96,0x9B,0x4C,0xE5,0xFF,0x01};
uint8_t spMORNING[] PROGMEM = {0xCE,0x08,0x52,0x2A,0x35,0x5D,0x39,0x53,0x29,0x5B,0xB7,0x0A,0x15,0x0C,0xEE,0x2A,0x42,0x56,0x66,0xD2,0x55,0x2E,0x37,0x2F,0xD9,0x45,0xB3,0xD3,0xC5,0xCA,0x6D,0x27,0xD5,0xEE,0x50,0xF5,0x50,0x94,0x14,0x77,0x2D,0xD8,0x5D,0x49,0x92,0xFD,0xB1,0x64,0x2F,0xA9,0x49,0x0C,0x93,0x4B,0xAD,0x19,0x17,0x3E,0x66,0x1E,0xF1,0xA2,0x5B,0x84,0xE2,0x29,0x8F,0x8B,0x72,0x10,0xB5,0xB1,0x2E,0x4B,0xD4,0x45,0x89,0x4A,0xEC,0x5C,0x95,0x14,0x2B,0x8A,0x9C,0x34,0x52,0x5D,0xBC,0xCC,0xB5,0x3B,0x49,0x69,0x89,0x87,0xC1,0x98,0x56,0x3A,0x21,0x2B,0x82,0x67,0xCC,0x5C,0x85,0xB5,0x4A,0x8A,0xF6,0x64,0xA9,0x96,0xC4,0x69,0x3C,0x52,0x81,0x58,0x1C,0x97,0xF6,0x0E,0x1B,0xCC,0x0D,0x42,0x32,0xAA,0x65,0x12,0x67,0xD4,0x6A,0x61,0x52,0xFC,0xFF};
uint8_t spAFTERNOON[] PROGMEM = {0xC7,0xCE,0xCE,0x3A,0xCB,0x58,0x1F,0x3B,0x07,0x9D,0x28,0x71,0xB4,0xAC,0x9C,0x74,0x5A,0x42,0x55,0x33,0xB2,0x93,0x0A,0x09,0xD4,0xC5,0x9A,0xD6,0x44,0x45,0xE3,0x38,0x60,0x9A,0x32,0x05,0xF4,0x18,0x01,0x09,0xD8,0xA9,0xC2,0x00,0x5E,0xCA,0x24,0xD5,0x5B,0x9D,0x4A,0x95,0xEA,0x34,0xEE,0x63,0x92,0x5C,0x4D,0xD0,0xA4,0xEE,0x58,0x0C,0xB9,0x4D,0xCD,0x42,0xA2,0x3A,0x24,0x37,0x25,0x8A,0xA8,0x8E,0xA0,0x53,0xE4,0x28,0x23,0x26,0x13,0x72,0x91,0xA2,0x76,0xBB,0x72,0x38,0x45,0x0A,0x46,0x63,0xCA,0x69,0x27,0x39,0x58,0xB1,0x8D,0x60,0x1C,0x34,0x1B,0x34,0xC3,0x55,0x8E,0x73,0x45,0x2D,0x4F,0x4A,0x3A,0x26,0x10,0xA1,0xCA,0x2D,0xE9,0x98,0x24,0x0A,0x1E,0x6D,0x97,0x29,0xD2,0xCC,0x71,0xA2,0xDC,0x86,0xC8,0x12,0xA7,0x8E,0x08,0x85,0x22,0x8D,0x9C,0x43,0xA7,0x12,0xB2,0x2E,0x50,0x09,0xEF,0x51,0xC5,0xBA,0x28,0x58,0xAD,0xDB,0xE1,0xFF,0x03};
uint8_t spEVENING[] PROGMEM = {0xCD,0x6D,0x98,0x73,0x47,0x65,0x0D,0x6D,0x10,0xB2,0x5D,0x93,0x35,0x94,0xC1,0xD0,0x76,0x4D,0x66,0x93,0xA7,0x04,0xBD,0x71,0xD9,0x45,0xAE,0x92,0xD5,0xAC,0x53,0x07,0x6D,0xA5,0x76,0x63,0x51,0x92,0xD4,0xA1,0x83,0xD4,0xCB,0xB2,0x51,0x88,0xCD,0xF5,0x50,0x45,0xCE,0xA2,0x2E,0x27,0x28,0x54,0x15,0x37,0x0A,0xCF,0x75,0x61,0x5D,0xA2,0xC4,0xB5,0xC7,0x44,0x55,0x8A,0x0B,0xA3,0x6E,0x17,0x95,0x21,0xA9,0x0C,0x37,0xCD,0x15,0xBA,0xD4,0x2B,0x6F,0xB3,0x54,0xE4,0xD2,0xC8,0x64,0xBC,0x4C,0x91,0x49,0x12,0xE7,0xB2,0xB1,0xD0,0x22,0x0D,0x9C,0xDD,0xAB,0x62,0xA9,0x38,0x53,0x11,0xA9,0x74,0x2C,0xD2,0xCA,0x59,0x34,0xA3,0xE5,0xFF,0x03};
uint8_t spPAUSE1[] PROGMEM = {0x00,0x00,0x00,0x00,0xFF,0x0F};
void sayTime(int hour, int minutes, AudioGeneratorTalkie *talkie)
{
bool pm = (hour >= 12);
uint8_t *spHour[] = { spTWELVE, spONE, spTWO, spTHREE, spFOUR, spFIVE, spSIX,
spSEVEN, spEIGHT, spNINE, spTEN, spELEVEN };
size_t spHourLen[] = { sizeof(spTWELVE), sizeof(spONE), sizeof(spTWO),
sizeof(spTHREE), sizeof(spFOUR), sizeof(spFIVE),
sizeof(spSIX), sizeof(spSEVEN), sizeof(spEIGHT),
sizeof(spNINE), sizeof(spTEN), sizeof(spELEVEN) };
uint8_t *spMinDec[] = { spOH, spTEN, spTWENTY, spTHIRTY, spFOURTY, spFIFTY };
size_t spMinDecLen[] = { sizeof(spOH), sizeof(spTEN), sizeof(spTWENTY),
sizeof(spTHIRTY), sizeof(spFOURTY), sizeof(spFIFTY) };
uint8_t *spMinSpecial[] = { spELEVEN, spTWELVE, spTHIRTEEN, spFOURTEEN,
spFIFTEEN, spSIXTEEN, spSEVENTEEN, spEIGHTEEN,
spNINETEEN };
size_t spMinSpecialLen[] = { sizeof(spELEVEN), sizeof(spTWELVE),
sizeof(spTHIRTEEN), sizeof(spFOURTEEN),
sizeof(spFIFTEEN), sizeof(spSIXTEEN),
sizeof(spSEVENTEEN), sizeof(spEIGHTEEN),
sizeof(spNINETEEN) };
uint8_t *spMinLow[] = { spONE, spTWO, spTHREE, spFOUR, spFIVE, spSIX,
spSEVEN, spEIGHT, spNINE };
size_t spMinLowLen[] = { sizeof(spONE), sizeof(spTWO), sizeof(spTHREE),
sizeof(spFOUR), sizeof(spFIVE), sizeof(spSIX),
sizeof(spSEVEN), sizeof(spEIGHT), sizeof(spNINE) };
talkie->say(spTHE, sizeof(spTHE));
talkie->say(spTIME, sizeof(spTIME));
talkie->say(spIS, sizeof(spIS));
hour = hour % 12;
talkie->say(spHour[hour], spHourLen[hour]);
if (minutes==0) {
talkie->say(spOCLOCK, sizeof(spOCLOCK));
} else if (minutes<=10 || minutes >=20) {
talkie->say(spMinDec[minutes / 10], spMinDecLen[minutes /10]);
if (minutes % 10) {
talkie->say(spMinLow[(minutes % 10) - 1], spMinLowLen[(minutes % 10) - 1]);
}
} else {
talkie->say(spMinSpecial[minutes - 11], spMinSpecialLen[minutes - 11]);
}
if (pm) {
talkie->say(spP_M_, sizeof(spP_M_));
} else {
talkie->say(spA_M_, sizeof(spA_M_));
}
}
AudioGeneratorTalkie *talkie;
AudioOutputI2S *out;
bool GetLocalTime(struct tm * info, uint32_t ms) {
uint32_t count = ms / 10;
time_t now;
time(&now);
localtime_r(&now, info);
if (info->tm_year > (2016 - 1900)) {
return true;
}
while (count--) {
delay(10);
time(&now);
localtime_r(&now, info);
if (info->tm_year > (2016 - 1900)) {
return true;
}
}
return false;
}
void setup()
{
Serial.begin(115200);
delay(1000);
// We start by connecting to a WiFi network
Serial.println();
Serial.println();
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.begin(ssid, pass);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("WiFi connected");
Serial.println("IP address: ");
Serial.println(WiFi.localIP());
Serial.println("Contacting Time Server");
configTime(3600 * timezone, daysavetime * 3600, "time.nist.gov", "0.pool.ntp.org", "1.pool.ntp.org");
struct tm tmstruct ;
do {
tmstruct.tm_year = 0;
Serial.printf(".");
GetLocalTime(&tmstruct, 5000);
delay(100);
} while (tmstruct.tm_year < 100);
audioLogger = &Serial;
out = new AudioOutputI2S();
talkie = new AudioGeneratorTalkie();
talkie->begin(nullptr, out);
}
void loop()
{
struct tm tmstruct ;
tmstruct.tm_year = 0;
GetLocalTime(&tmstruct, 5000);
Serial.printf("\nNow is : %d-%02d-%02d %02d:%02d:%02d\n",
tmstruct.tm_year + 1900, tmstruct.tm_mon + 1,
tmstruct.tm_mday, tmstruct.tm_hour,
tmstruct.tm_min, tmstruct.tm_sec);
sayTime(tmstruct.tm_hour, tmstruct.tm_min, talkie);
delay(1000);
}

View File

@ -0,0 +1,441 @@
/*
WebRadio Example
Very simple HTML app to control web streaming
Copyright (C) 2017 Earle F. Philhower, III
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <Arduino.h>
#ifdef ESP32
#include <WiFi.h>
#else
#include <ESP8266WiFi.h>
#endif
#include "AudioFileSourceICYStream.h"
#include "AudioFileSourceBuffer.h"
#include "AudioGeneratorMP3.h"
#include "AudioGeneratorAAC.h"
#include "AudioOutputI2S.h"
#include <EEPROM.h>
// Custom web server that doesn't need much RAM
#include "web.h"
// To run, set your ESP8266 build to 160MHz, update the SSID info, and upload.
// Enter your WiFi setup here:
#ifndef STASSID
#define STASSID "your-ssid"
#define STAPSK "your-password"
#endif
const char* ssid = STASSID;
const char* password = STAPSK;
WiFiServer server(80);
AudioGenerator *decoder = NULL;
AudioFileSourceICYStream *file = NULL;
AudioFileSourceBuffer *buff = NULL;
AudioOutputI2S *out = NULL;
int volume = 100;
char title[64];
char url[96];
char status[64];
bool newUrl = false;
bool isAAC = false;
int retryms = 0;
typedef struct {
char url[96];
bool isAAC;
int16_t volume;
int16_t checksum;
} Settings;
// C++11 multiline string constants are neato...
static const char HEAD[] PROGMEM = R"KEWL(
<head>
<title>ESP8266 Web Radio</title>
<script type="text/javascript">
function updateTitle() {
var x = new XMLHttpRequest();
x.open("GET", "title");
x.onload = function() { document.getElementById("titlespan").innerHTML=x.responseText; setTimeout(updateTitle, 5000); }
x.onerror = function() { setTimeout(updateTitle, 5000); }
x.send();
}
setTimeout(updateTitle, 1000);
function showValue(n) {
document.getElementById("volspan").innerHTML=n;
var x = new XMLHttpRequest();
x.open("GET", "setvol?vol="+n);
x.send();
}
function updateStatus() {var x = new XMLHttpRequest();
x.open("GET", "status");
x.onload = function() { document.getElementById("statusspan").innerHTML=x.responseText; setTimeout(updateStatus, 5000); }
x.onerror = function() { setTimeout(updateStatus, 5000); }
x.send();
}
setTimeout(updateStatus, 2000);
</script>
</head>)KEWL";
static const char BODY[] PROGMEM = R"KEWL(
<body>
ESP8266 Web Radio!
<hr>
Currently Playing: <span id="titlespan">%s</span><br>
Volume: <input type="range" name="vol" min="1" max="150" steps="10" value="%d" onchange="showValue(this.value)"/> <span id="volspan">%d</span>%%
<hr>
Status: <span id="statusspan">%s</span>
<hr>
<form action="changeurl" method="GET">
Current URL: %s<br>
Change URL: <input type="text" name="url">
<select name="type"><option value="mp3">MP3</option><option value="aac">AAC</option></select>
<input type="submit" value="Change"></form>
<form action="stop" method="POST"><input type="submit" value="Stop"></form>
</body>)KEWL";
void HandleIndex(WiFiClient *client)
{
char buff[sizeof(BODY) + sizeof(title) + sizeof(status) + sizeof(url) + 3*2];
Serial.printf_P(PSTR("Sending INDEX...Free mem=%d\n"), ESP.getFreeHeap());
WebHeaders(client, NULL);
WebPrintf(client, DOCTYPE);
client->write_P( PSTR("<html>"), 6 );
client->write_P( HEAD, strlen_P(HEAD) );
sprintf_P(buff, BODY, title, volume, volume, status, url);
client->write(buff, strlen(buff) );
client->write_P( PSTR("</html>"), 7 );
Serial.printf_P(PSTR("Sent INDEX...Free mem=%d\n"), ESP.getFreeHeap());
}
void HandleStatus(WiFiClient *client)
{
WebHeaders(client, NULL);
client->write(status, strlen(status));
}
void HandleTitle(WiFiClient *client)
{
WebHeaders(client, NULL);
client->write(title, strlen(title));
}
void HandleVolume(WiFiClient *client, char *params)
{
char *namePtr;
char *valPtr;
while (ParseParam(&params, &namePtr, &valPtr)) {
ParamInt("vol", volume);
}
Serial.printf_P(PSTR("Set volume: %d\n"), volume);
out->SetGain(((float)volume)/100.0);
RedirectToIndex(client);
}
void HandleChangeURL(WiFiClient *client, char *params)
{
char *namePtr;
char *valPtr;
char newURL[sizeof(url)];
char newType[4];
newURL[0] = 0;
newType[0] = 0;
while (ParseParam(&params, &namePtr, &valPtr)) {
ParamText("url", newURL);
ParamText("type", newType);
}
if (newURL[0] && newType[0]) {
newUrl = true;
strncpy(url, newURL, sizeof(url)-1);
url[sizeof(url)-1] = 0;
if (!strcmp_P(newType, PSTR("aac"))) {
isAAC = true;
} else {
isAAC = false;
}
strcpy_P(status, PSTR("Changing URL..."));
Serial.printf_P(PSTR("Changed URL to: %s(%s)\n"), url, newType);
RedirectToIndex(client);
} else {
WebError(client, 404, NULL, false);
}
}
void RedirectToIndex(WiFiClient *client)
{
WebError(client, 301, PSTR("Location: /\r\n"), true);
}
void StopPlaying()
{
if (decoder) {
decoder->stop();
delete decoder;
decoder = NULL;
}
if (buff) {
buff->close();
delete buff;
buff = NULL;
}
if (file) {
file->close();
delete file;
file = NULL;
}
strcpy_P(status, PSTR("Stopped"));
strcpy_P(title, PSTR("Stopped"));
}
void HandleStop(WiFiClient *client)
{
Serial.printf_P(PSTR("HandleStop()\n"));
StopPlaying();
RedirectToIndex(client);
}
void MDCallback(void *cbData, const char *type, bool isUnicode, const char *str)
{
const char *ptr = reinterpret_cast<const char *>(cbData);
(void) isUnicode; // Punt this ball for now
(void) ptr;
if (strstr_P(type, PSTR("Title"))) {
strncpy(title, str, sizeof(title));
title[sizeof(title)-1] = 0;
} else {
// Who knows what to do? Not me!
}
}
void StatusCallback(void *cbData, int code, const char *string)
{
const char *ptr = reinterpret_cast<const char *>(cbData);
(void) code;
(void) ptr;
strncpy_P(status, string, sizeof(status)-1);
status[sizeof(status)-1] = 0;
}
#ifdef ESP8266
const int preallocateBufferSize = 5*1024;
const int preallocateCodecSize = 29192; // MP3 codec max mem needed
#else
const int preallocateBufferSize = 16*1024;
const int preallocateCodecSize = 85332; // AAC+SBR codec max mem needed
#endif
void *preallocateBuffer = NULL;
void *preallocateCodec = NULL;
void setup()
{
// First, preallocate all the memory needed for the buffering and codecs, never to be freed
preallocateBuffer = malloc(preallocateBufferSize);
preallocateCodec = malloc(preallocateCodecSize);
if (!preallocateBuffer || !preallocateCodec) {
Serial.begin(115200);
Serial.printf_P(PSTR("FATAL ERROR: Unable to preallocate %d bytes for app\n"), preallocateBufferSize+preallocateCodecSize);
while (1) delay(1000); // Infinite halt
}
Serial.begin(115200);
delay(1000);
Serial.printf_P(PSTR("Connecting to WiFi\n"));
WiFi.disconnect();
WiFi.softAPdisconnect(true);
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
// Try forever
while (WiFi.status() != WL_CONNECTED) {
Serial.printf_P(PSTR("...Connecting to WiFi\n"));
delay(1000);
}
Serial.printf_P(PSTR("Connected\n"));
Serial.printf_P(PSTR("Go to http://"));
Serial.print(WiFi.localIP());
Serial.printf_P(PSTR("/ to control the web radio.\n"));
server.begin();
strcpy_P(url, PSTR("none"));
strcpy_P(status, PSTR("OK"));
strcpy_P(title, PSTR("Idle"));
audioLogger = &Serial;
file = NULL;
buff = NULL;
out = new AudioOutputI2S();
decoder = NULL;
LoadSettings();
}
void StartNewURL()
{
Serial.printf_P(PSTR("Changing URL to: %s, vol=%d\n"), url, volume);
newUrl = false;
// Stop and free existing ones
Serial.printf_P(PSTR("Before stop...Free mem=%d\n"), ESP.getFreeHeap());
StopPlaying();
Serial.printf_P(PSTR("After stop...Free mem=%d\n"), ESP.getFreeHeap());
SaveSettings();
Serial.printf_P(PSTR("Saved settings\n"));
file = new AudioFileSourceICYStream(url);
Serial.printf_P(PSTR("created icystream\n"));
file->RegisterMetadataCB(MDCallback, NULL);
buff = new AudioFileSourceBuffer(file, preallocateBuffer, preallocateBufferSize);
Serial.printf_P(PSTR("created buffer\n"));
buff->RegisterStatusCB(StatusCallback, NULL);
decoder = isAAC ? (AudioGenerator*) new AudioGeneratorAAC(preallocateCodec, preallocateCodecSize) : (AudioGenerator*) new AudioGeneratorMP3(preallocateCodec, preallocateCodecSize);
Serial.printf_P(PSTR("created decoder\n"));
decoder->RegisterStatusCB(StatusCallback, NULL);
Serial.printf_P("Decoder start...\n");
decoder->begin(buff, out);
out->SetGain(((float)volume)/100.0);
if (!decoder->isRunning()) {
Serial.printf_P(PSTR("Can't connect to URL"));
StopPlaying();
strcpy_P(status, PSTR("Unable to connect to URL"));
retryms = millis() + 2000;
}
Serial.printf_P("Done start new URL\n");
}
void LoadSettings()
{
// Restore from EEPROM, check the checksum matches
Settings s;
uint8_t *ptr = reinterpret_cast<uint8_t *>(&s);
EEPROM.begin(sizeof(s));
for (size_t i=0; i<sizeof(s); i++) {
ptr[i] = EEPROM.read(i);
}
EEPROM.end();
int16_t sum = 0x1234;
for (size_t i=0; i<sizeof(url); i++) sum += s.url[i];
sum += s.isAAC;
sum += s.volume;
if (s.checksum == sum) {
strcpy(url, s.url);
isAAC = s.isAAC;
volume = s.volume;
Serial.printf_P(PSTR("Resuming stream from EEPROM: %s, type=%s, vol=%d\n"), url, isAAC?"AAC":"MP3", volume);
newUrl = true;
}
}
void SaveSettings()
{
// Store in "EEPROM" to restart automatically
Settings s;
memset(&s, 0, sizeof(s));
strcpy(s.url, url);
s.isAAC = isAAC;
s.volume = volume;
s.checksum = 0x1234;
for (size_t i=0; i<sizeof(url); i++) s.checksum += s.url[i];
s.checksum += s.isAAC;
s.checksum += s.volume;
uint8_t *ptr = reinterpret_cast<uint8_t *>(&s);
EEPROM.begin(sizeof(s));
for (size_t i=0; i<sizeof(s); i++) {
EEPROM.write(i, ptr[i]);
}
EEPROM.commit();
EEPROM.end();
}
void PumpDecoder()
{
if (decoder && decoder->isRunning()) {
strcpy_P(status, PSTR("Playing")); // By default we're OK unless the decoder says otherwise
if (!decoder->loop()) {
Serial.printf_P(PSTR("Stopping decoder\n"));
StopPlaying();
retryms = millis() + 2000;
}
}
}
void loop()
{
static int lastms = 0;
if (millis()-lastms > 1000) {
lastms = millis();
Serial.printf_P(PSTR("Running for %d seconds%c...Free mem=%d\n"), lastms/1000, !decoder?' ':(decoder->isRunning()?'*':' '), ESP.getFreeHeap());
}
if (retryms && millis()-retryms>0) {
retryms = 0;
newUrl = true;
}
if (newUrl) {
StartNewURL();
}
PumpDecoder();
char *reqUrl;
char *params;
WiFiClient client = server.available();
PumpDecoder();
char reqBuff[384];
if (client && WebReadRequest(&client, reqBuff, 384, &reqUrl, &params)) {
PumpDecoder();
if (IsIndexHTML(reqUrl)) {
HandleIndex(&client);
} else if (!strcmp_P(reqUrl, PSTR("stop"))) {
HandleStop(&client);
} else if (!strcmp_P(reqUrl, PSTR("status"))) {
HandleStatus(&client);
} else if (!strcmp_P(reqUrl, PSTR("title"))) {
HandleTitle(&client);
} else if (!strcmp_P(reqUrl, PSTR("setvol"))) {
HandleVolume(&client, params);
} else if (!strcmp_P(reqUrl, PSTR("changeurl"))) {
HandleChangeURL(&client, params);
} else {
WebError(&client, 404, NULL, false);
}
// web clients hate when door is violently shut
while (client.available()) {
PumpDecoder();
client.read();
}
}
PumpDecoder();
if (client) {
client.flush();
client.stop();
}
}

View File

@ -0,0 +1,314 @@
/*
PsychoPlug
ESP8266 based remote outlet with standalone timer and MQTT integration
Copyright (C) 2017 Earle F. Philhower, III
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <Arduino.h>
#ifdef ESP32
#include <WiFi.h>
#else
#include <ESP8266WiFi.h>
#endif
#include "web.h"
void WebPrintError(WiFiClient *client, int code)
{
switch(code) {
case 301: WebPrintf(client, "301 Moved Permanently"); break;
case 400: WebPrintf(client, "400 Bad Request"); break;
case 401: WebPrintf(client, "401 Unauthorized"); break;
case 404: WebPrintf(client, "404 Not Found"); break;
case 405: WebPrintf(client, "405 Method Not Allowed"); break;
default: WebPrintf(client, "500 Server Error"); break;
}
}
void WebError(WiFiClient *client, int code, const char *headers, bool usePMEM)
{
WebPrintf(client, "HTTP/1.1 %d\r\n", code);
WebPrintf(client, "Server: PsychoPlug\r\n");
WebPrintf(client, "Content-type: text/html\r\n");
WebPrintf(client, "Cache-Control: no-cache, no-store, must-revalidate\r\n");
WebPrintf(client, "Pragma: no-cache\r\n");
WebPrintf(client, "Expires: 0\r\n");
WebPrintf(client, "Connection: close\r\n");
if (headers) {
if (!usePMEM) {
WebPrintf(client, "%s", headers);
} else {
WebPrintfPSTR(client, headers);
}
}
WebPrintf(client, "\r\n\r\n");
WebPrintf(client, DOCTYPE);
WebPrintf(client, "<html><head><title>");
WebPrintError(client, code);
WebPrintf(client, "</title>" ENCODING "</head>\n");
WebPrintf(client, "<body><h1>");
WebPrintError(client, code);
WebPrintf(client, "</h1></body></html>\r\n");
}
void WebHeaders(WiFiClient *client, PGM_P /*const char **/headers)
{
WebPrintf(client, "HTTP/1.1 200 OK\r\n");
WebPrintf(client, "Server: PsychoPlug\r\n");
WebPrintf(client, "Content-type: text/html\r\n");
WebPrintf(client, "Cache-Control: no-cache, no-store, must-revalidate\r\n");
WebPrintf(client, "Pragma: no-cache\r\n");
WebPrintf(client, "Connection: close\r\n");
WebPrintf(client, "Expires: 0\r\n");
if (headers) {
WebPrintfPSTR(client, headers);
}
WebPrintf(client, "\r\n");
}
// In-place decoder, overwrites source with decoded values. Needs 0-termination on input
// Try and keep memory needs low, speed not critical
static uint8_t b64lut(uint8_t i)
{
if (i >= 'A' && i <= 'Z') return i - 'A';
if (i >= 'a' && i <= 'z') return i - 'a' + 26;
if (i >= '0' && i <= '9') return i - '0' + 52;
if (i == '-') return 62;
if (i == '_') return 63;
else return 64;// sentinel
}
void Base64Decode(char *str)
{
char *dest;
dest = str;
if (strlen(str)%4) return; // Not multiple of 4 == error
while (*str) {
uint8_t a = b64lut(*(str++));
uint8_t b = b64lut(*(str++));
uint8_t c = b64lut(*(str++));
uint8_t d = b64lut(*(str++));
*(dest++) = (a << 2) | ((b & 0x30) >> 4);
if (c == 64) break;
*(dest++) = ((b & 0x0f) << 4) | ((c & 0x3c) >> 2);
if (d == 64) break;
*(dest++) = ((c & 0x03) << 6) | d;
}
*dest = 0; // Terminate the string
}
void URLDecode(char *ptr)
{
while (*ptr) {
if (*ptr == '+') {
*ptr = ' ';
} else if (*ptr == '%') {
if (*(ptr+1) && *(ptr+2)) {
byte a = *(ptr + 1);
byte b = *(ptr + 2);
if (a>='0' && a<='9') a -= '0';
else if (a>='a' && a<='f') a = a - 'a' + 10;
else if (a>='A' && a<='F') a = a - 'A' + 10;
if (b>='0' && b<='9') b -= '0';
else if (b>='a' && b<='f') b = b - 'a' + 10;
else if (b>='A' && b<='F') b = b - 'A' + 10;
*ptr = ((a&0x0f)<<4) | (b&0x0f);
// Safe strcpy the rest of the string back
char *p1 = ptr + 1;
char *p2 = ptr + 3;
while (*p2) { *p1 = *p2; p1++; p2++; }
*p1 = 0;
}
// OTW this is a bad encoding, just pass unchanged
}
ptr++;
}
}
// Parse HTTP request
bool WebReadRequest(WiFiClient *client, char *reqBuff, int reqBuffLen, char **urlStr, char **paramStr)
{
static char NUL = 0; // Get around writable strings...
*urlStr = NULL;
*paramStr = NULL;
unsigned long timeoutMS = millis() + 5000; // Max delay before we timeout
while (!client->available() && millis() < timeoutMS) { delay(10); }
if (!client->available()) {
return false;
}
int wlen = client->readBytesUntil('\r', reqBuff, reqBuffLen-1);
reqBuff[wlen] = 0;
// Delete HTTP version (well, anything after the 2nd space)
char *ptr = reqBuff;
while (*ptr && *ptr!=' ') ptr++;
if (*ptr) ptr++;
while (*ptr && *ptr!=' ') ptr++;
*ptr = 0;
URLDecode(reqBuff);
char *url;
char *qp;
if (!memcmp_P(reqBuff, PSTR("GET "), 4)) {
client->flush(); // Don't need anything here...
// Break into URL and form data
url = reqBuff+4;
while (*url && *url=='/') url++; // Strip off leading /s
qp = strchr(url, '?');
if (qp) {
*qp = 0; // End URL
qp++;
} else {
qp = &NUL;
}
} else if (!memcmp_P(reqBuff, PSTR("POST "), 5)) {
uint8_t newline;
client->read(&newline, 1); // Get rid of \n
url = reqBuff+5;
while (*url && *url=='/') url++; // Strip off leading /s
qp = strchr(url, '?');
if (qp) *qp = 0; // End URL @ ?
// In a POST the params are in the body
int sizeleft = reqBuffLen - strlen(reqBuff) - 1;
qp = reqBuff + strlen(reqBuff) + 1;
int wlen = client->readBytesUntil('\r', qp, sizeleft-1);
qp[wlen] = 0;
client->flush();
URLDecode(qp);
} else {
// Not a GET or POST, error
WebError(client, 405, PSTR("Allow: GET, POST"));
return false;
}
if (urlStr) *urlStr = url;
if (paramStr) *paramStr = qp;
return true;
}
// Scan out and update a pointeinto the param string, returning the name and value or false if done
bool ParseParam(char **paramStr, char **name, char **value)
{
char *data = *paramStr;
if (*data==0) return false;
char *namePtr = data;
while ((*data != 0) && (*data != '=') && (*data != '&')) data++;
if (*data) { *data = 0; data++; }
char *valPtr = data;
if (*data == '=') data++;
while ((*data != 0) && (*data != '=') && (*data != '&')) data++;
if (*data) { *data = 0; data++;}
*paramStr = data;
*name = namePtr;
*value = valPtr;
return true;
}
bool IsIndexHTML(const char *url)
{
if (!url) return false;
if (*url==0 || !strcmp_P(url, PSTR("/")) || !strcmp_P(url, PSTR("/index.html")) || !strcmp_P(url, PSTR("index.html"))) return true;
else return false;
}
void WebFormText(WiFiClient *client, /*const char **/ PGM_P label, const char *name, const char *value, bool enabled)
{
WebPrintfPSTR(client, label);
WebPrintf(client, ": <input type=\"text\" name=\"%s\" id=\"%s\" value=\"%s\" %s><br>\n", name, name, value, !enabled?"disabled":"");
}
void WebFormText(WiFiClient *client, /*const char **/ PGM_P label, const char *name, const int value, bool enabled)
{
WebPrintfPSTR(client, label);
WebPrintf(client, ": <input type=\"text\" name=\"%s\" id=\"%s\" value=\"%d\" %s><br>\n", name, name, value, !enabled?"disabled":"");
}
void WebFormCheckbox(WiFiClient *client, /*const char **/ PGM_P label, const char *name, bool checked, bool enabled)
{
WebPrintf(client, "<input type=\"checkbox\" name=\"%s\" id=\"%s\" %s %s> ", name, name, checked?"checked":"", !enabled?"disabled":"");
WebPrintfPSTR(client, label);
WebPrintf(client, "<br>\n");
}
void WebFormCheckboxDisabler(WiFiClient *client, PGM_P /*const char **/label, const char *name, bool invert, bool checked, bool enabled, const char *ids[])
{
WebPrintf(client,"<input type=\"checkbox\" name=\"%s\" id=\"%s\" onclick=\"", name,name);
if (invert) WebPrintf(client, "var x = true; if (this.checked) { x = false; }\n")
else WebPrintf(client, "var x = false; if (this.checked) { x = true; }\n");
for (byte i=0; ids[i][0]; i++ ) {
WebPrintf(client, "document.getElementById('%s').disabled = x;\n", ids[i]);
}
WebPrintf(client, "\" %s %s> ", checked?"checked":"", !enabled?"disabled":"")
WebPrintfPSTR(client, label);
WebPrintf(client, "<br>\n");
}
// Scan an integer from a string, place it into dest, and then return # of bytes scanned
int ParseInt(char *src, int *dest)
{
byte count = 0;
bool neg = false;
int res = 0;
if (!src) return 0;
if (src[0] == '-') {neg = true; src++; count++;}
while (*src && (*src>='0') && (*src<='9')) {
res = res * 10;
res += *src - '0';
src++;
count++;
}
if (neg) res *= -1;
if (dest) *dest = res;
return count;
}
void Read4Int(char *str, byte *p)
{
int i;
str += ParseInt(str, &i); p[0] = i; if (*str) str++;
str += ParseInt(str, &i); p[1] = i; if (*str) str++;
str += ParseInt(str, &i); p[2] = i; if (*str) str++;
str += ParseInt(str, &i); p[3] = i;
}

View File

@ -0,0 +1,64 @@
/*
PsychoPlug
ESP8266 based remote outlet with standalone timer and MQTT integration
Copyright (C) 2017 Earle F. Philhower, III
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef _web_h
#define _web_h
// Global way of writing out dynamic HTML to socket
// snprintf guarantees a null termination
#define WebPrintf(c, fmt, ...) { char webBuff[192]; snprintf_P(webBuff, sizeof(webBuff), PSTR(fmt), ## __VA_ARGS__); (c)->print(webBuff); delay(10);}
#define WebPrintfPSTR(c, fmt, ...) { char webBuff[192]; snprintf_P(webBuff, sizeof(webBuff), (fmt), ## __VA_ARGS__); (c)->print(webBuff); delay(10);}
// Common HTTP header bits
#define DOCTYPE "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\">"
#define ENCODING "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"/>\n"
// Web header creation
void WebPrintError(WiFiClient *client, int code); // Sends only the error code string and a description
void WebError(WiFiClient *client, int code, const char *headers, bool usePMEM = true); // Sends whole HTTP error headers
void WebHeaders(WiFiClient *client, PGM_P /*const char **/headers); // Send success headers
// Web decoding utilities
void Base64Decode(char *str); // In-place B64 decode
void URLDecode(char *ptr); // In-place URL decode
// GET/POST parsing
bool WebReadRequest(WiFiClient *client, char *reqBuff, int reqBuffLen, char **urlStr, char **paramStr);
bool ParseParam(char **paramStr, char **name, char **value); // Get next name/parameter from a param string
bool IsIndexHTML(const char *url); // Is this meant to be index.html (/, index.htm, etc.)
// HTML FORM generation
void WebFormText(WiFiClient *client, /*const char **/ PGM_P label, const char *name, const char *value, bool enabled);
void WebFormText(WiFiClient *client, /*const char **/ PGM_P label, const char *name, const int value, bool enabled);
void WebFormCheckbox(WiFiClient *client, /*const char **/ PGM_P label, const char *name, bool checked, bool enabled);
void WebFormCheckboxDisabler(WiFiClient *client, PGM_P /*const char **/label, const char *name, bool invert, bool checked, bool enabled, const char *ids[]);
// HTML FORM parsing
int ParseInt(char *src, int *dest);
void Read4Int(char *str, byte *p);
#define ParamText(name, dest) { if (!strcmp(namePtr, (name))) strlcpy((dest), valPtr, sizeof(dest)); }
#define ParamCheckbox(name, dest) { if (!strcmp(namePtr, (name))) (dest) = !strcmp("on", valPtr); }
#define ParamInt(name, dest) { if (!strcmp(namePtr, (name))) ParseInt(valPtr, &dest); }
#define Param4Int(name, dest) { if (!strcmp(namePtr, (name))) Read4Int(valPtr, (dest)); }
#endif

31
lib/ESP8266Audio/keywords.txt Executable file
View File

@ -0,0 +1,31 @@
AudioFileSource KEYWORD1
AudioFileSourceSPIFFS KEYWORD1
AudioFileSourceLittleFS KEYWORD1
AudioFileSourceFS KEYWORD1
AudioFileSourcePROGMEM KEYWORD1
AudioFileSourceHTTPStream KEYWORD1
AudioFileSourceICYStream KEYWORD1
AudioFileSourceID3 KEYWORD1
AudioFileSourceSD KEYWORD1
AudioFileSourceBuffer KEYWORD1
AudioFileSourceSPIRAMBuffer KEYWORD1
AudioGenerator KEYWORD1
AudioGeneratorAAC KEYWORD1
AudioGeneratorFLAC KEYWORD1
AudioGeneratorMOD KEYWORD1
AudioGeneratorMIDI KEYWORD1
AudioGeneratorMP3 KEYWORD1
AudioGeneratorOpus KEYWORD1
AudioGeneratorRTTTL KEYWORD1
AudioGeneratorTalkie KEYWORD1
AudioGeneratorWAV KEYWORD1
AudioOutput KEYWORD1
AudioOutputI2S KEYWORD1
AudioOutputI2SNoDAC KEYWORD1
AudioOutputNull KEYWORD1
AudioOutputBuffer KEYWORD1
AudioOutputSerialWAV KEYWORD1
AudioOutputSPIFFSWAV KEYWORD1
AudioOutputMixer KEYWORD1
AudioOutputMixerStub KEYWORD1
AudioOutputSPDIF KEYWORD1

26
lib/ESP8266Audio/library.json Executable file
View File

@ -0,0 +1,26 @@
{
"name": "ESP8266Audio",
"description": "Audio file format and I2S DAC library",
"keywords": "ESP8266, ESP32, MP3, AAC, WAV, MOD, FLAC, RTTTL, MIDI, I2S, DAC, Delta-Sigma, TTS",
"authors": [
{
"name": "Earle F. Philhower, III",
"email": "earlephilhower@yahoo.com",
"url": "https://github.com/earlephilhower/ESP8266Audio",
"maintainer": true
}
],
"repository": {
"type": "git",
"url": "https://github.com/earlephilhower/ESP8266Audio"
},
"version": "1.5.0",
"homepage": "https://github.com/earlephilhower/ESP8266Audio",
"dependencies": {
"SPI": "1.0"
},
"frameworks": "Arduino",
"examples": [
"examples/*/*.ino"
]
}

View File

@ -0,0 +1,9 @@
name=ESP8266Audio
version=1.5.0
author=Earle F. Philhower, III
maintainer=Earle F. Philhower, III
sentence=Audio file and I2S sound playing routines.
paragraph=Decode compressed MP3, AAC, FLAC, Screamtracker MOD, MIDI, RTTL, TI Talkie, and WAV and play on an I2S DAC or a software-driven delta-sigma DAC and 1-transistor amplifier.
category=Signal Input/Output
url=https://github.com/earlephilhower/ESP8266Audio
architectures=esp8266,esp32

View File

@ -0,0 +1,51 @@
/*
AudioFileSource
Base class of an input "file" to be used by AudioGenerator
Copyright (C) 2017 Earle F. Philhower, III
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef _AUDIOFILESOURCE_H
#define _AUDIOFILESOURCE_H
#include <Arduino.h>
#include "AudioStatus.h"
class AudioFileSource
{
public:
AudioFileSource() {};
virtual ~AudioFileSource() {};
virtual bool open(const char *filename) { (void)filename; return false; };
virtual uint32_t read(void *data, uint32_t len) { (void)data; (void)len; return 0; };
virtual uint32_t readNonBlock(void *data, uint32_t len) { return read(data, len); };
virtual bool seek(int32_t pos, int dir) { (void)pos; (void)dir; return false; };
virtual bool close() { return false; };
virtual bool isOpen() { return false; };
virtual uint32_t getSize() { return 0; };
virtual uint32_t getPos() { return 0; };
virtual bool loop() { return true; };
public:
virtual bool RegisterMetadataCB(AudioStatus::metadataCBFn fn, void *data) { return cb.RegisterMetadataCB(fn, data); }
virtual bool RegisterStatusCB(AudioStatus::statusCBFn fn, void *data) { return cb.RegisterStatusCB(fn, data); }
protected:
AudioStatus cb;
};
#endif

View File

@ -0,0 +1,190 @@
/*
AudioFileSourceBuffer
Double-buffered file source using system RAM
Copyright (C) 2017 Earle F. Philhower, III
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <Arduino.h>
#include "AudioFileSourceBuffer.h"
#pragma GCC optimize ("O3")
AudioFileSourceBuffer::AudioFileSourceBuffer(AudioFileSource *source, uint32_t buffSizeBytes)
{
buffSize = buffSizeBytes;
buffer = (uint8_t*)malloc(sizeof(uint8_t) * buffSize);
if (!buffer) audioLogger->printf_P(PSTR("Unable to allocate AudioFileSourceBuffer::buffer[]\n"));
deallocateBuffer = true;
writePtr = 0;
readPtr = 0;
src = source;
length = 0;
filled = false;
}
AudioFileSourceBuffer::AudioFileSourceBuffer(AudioFileSource *source, void *inBuff, uint32_t buffSizeBytes)
{
buffSize = buffSizeBytes;
buffer = (uint8_t*)inBuff;
deallocateBuffer = false;
writePtr = 0;
readPtr = 0;
src = source;
length = 0;
filled = false;
}
AudioFileSourceBuffer::~AudioFileSourceBuffer()
{
if (deallocateBuffer) free(buffer);
buffer = NULL;
}
bool AudioFileSourceBuffer::seek(int32_t pos, int dir)
{
if(dir == SEEK_CUR && (readPtr+pos) < length) {
readPtr += pos;
return true;
} else {
// Invalidate
readPtr = 0;
writePtr = 0;
length = 0;
return src->seek(pos, dir);
}
}
bool AudioFileSourceBuffer::close()
{
if (deallocateBuffer) free(buffer);
buffer = NULL;
return src->close();
}
bool AudioFileSourceBuffer::isOpen()
{
return src->isOpen();
}
uint32_t AudioFileSourceBuffer::getSize()
{
return src->getSize();
}
uint32_t AudioFileSourceBuffer::getPos()
{
return src->getPos();
}
uint32_t AudioFileSourceBuffer::getFillLevel()
{
return length;
}
uint32_t AudioFileSourceBuffer::read(void *data, uint32_t len)
{
if (!buffer) return src->read(data, len);
uint32_t bytes = 0;
if (!filled) {
// Fill up completely before returning any data at all
cb.st(STATUS_FILLING, PSTR("Refilling buffer"));
length = src->read(buffer, buffSize);
writePtr = length % buffSize;
filled = true;
}
// Pull from buffer until we've got none left or we've satisfied the request
uint8_t *ptr = reinterpret_cast<uint8_t*>(data);
uint32_t toReadFromBuffer = (len < length) ? len : length;
if ( (toReadFromBuffer > 0) && (readPtr >= writePtr) ) {
uint32_t toReadToEnd = (toReadFromBuffer < (uint32_t)(buffSize - readPtr)) ? toReadFromBuffer : (buffSize - readPtr);
memcpy(ptr, &buffer[readPtr], toReadToEnd);
readPtr = (readPtr + toReadToEnd) % buffSize;
len -= toReadToEnd;
length -= toReadToEnd;
ptr += toReadToEnd;
bytes += toReadToEnd;
toReadFromBuffer -= toReadToEnd;
}
if (toReadFromBuffer > 0) { // We know RP < WP at this point
memcpy(ptr, &buffer[readPtr], toReadFromBuffer);
readPtr = (readPtr + toReadFromBuffer) % buffSize;
len -= toReadFromBuffer;
length -= toReadFromBuffer;
ptr += toReadFromBuffer;
bytes += toReadFromBuffer;
toReadFromBuffer -= toReadFromBuffer;
}
if (len) {
// Still need more, try direct read from src
bytes += src->read(ptr, len);
// We're out of buffered data, need to force a complete refill. Thanks, @armSeb
readPtr = 0;
writePtr = 0;
length = 0;
filled = false;
cb.st(STATUS_UNDERFLOW, PSTR("Buffer underflow"));
}
fill();
return bytes;
}
void AudioFileSourceBuffer::fill()
{
if (!buffer) return;
if (length < buffSize) {
// Now try and opportunistically fill the buffer
if (readPtr > writePtr) {
if (readPtr == writePtr+1) return;
uint32_t bytesAvailMid = readPtr - writePtr - 1;
int cnt = src->readNonBlock(&buffer[writePtr], bytesAvailMid);
length += cnt;
writePtr = (writePtr + cnt) % buffSize;
return;
}
if (buffSize > writePtr) {
uint32_t bytesAvailEnd = buffSize - writePtr;
int cnt = src->readNonBlock(&buffer[writePtr], bytesAvailEnd);
length += cnt;
writePtr = (writePtr + cnt) % buffSize;
if (cnt != (int)bytesAvailEnd) return;
}
if (readPtr > 1) {
uint32_t bytesAvailStart = readPtr - 1;
int cnt = src->readNonBlock(&buffer[writePtr], bytesAvailStart);
length += cnt;
writePtr = (writePtr + cnt) % buffSize;
}
}
}
bool AudioFileSourceBuffer::loop()
{
if (!src->loop()) return false;
fill();
return true;
}

View File

@ -0,0 +1,62 @@
/*
AudioFileSourceBuffer
Double-buffered input file using system RAM
Copyright (C) 2017 Earle F. Philhower, III
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef _AUDIOFILESOURCEBUFFER_H
#define _AUDIOFILESOURCEBUFFER_H
#include "AudioFileSource.h"
class AudioFileSourceBuffer : public AudioFileSource
{
public:
AudioFileSourceBuffer(AudioFileSource *in, uint32_t bufferBytes);
AudioFileSourceBuffer(AudioFileSource *in, void *buffer, uint32_t bufferBytes); // Pre-allocated buffer by app
virtual ~AudioFileSourceBuffer() override;
virtual uint32_t read(void *data, uint32_t len) override;
virtual bool seek(int32_t pos, int dir) override;
virtual bool close() override;
virtual bool isOpen() override;
virtual uint32_t getSize() override;
virtual uint32_t getPos() override;
virtual bool loop() override;
virtual uint32_t getFillLevel();
enum { STATUS_FILLING=2, STATUS_UNDERFLOW };
private:
virtual void fill();
private:
AudioFileSource *src;
uint32_t buffSize;
uint8_t *buffer;
bool deallocateBuffer;
uint32_t writePtr;
uint32_t readPtr;
uint32_t length;
bool filled;
};
#endif

View File

@ -0,0 +1,64 @@
/*
AudioFileSourceFS
Input Arduion "file" to be used by AudioGenerator
Copyright (C) 2017 Earle F. Philhower, III
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef _AUDIOFILESOURCEFATFS_H
#define _AUDIOFILESOURCEFATFS_H
#ifdef ESP32
#include <Arduino.h>
#include <FS.h>
#include <FFat.h>
#include "AudioFileSource.h"
#include "AudioFileSourceFS.h"
/*
AudioFileSource for FAT filesystem.
*/
class AudioFileSourceFATFS : public AudioFileSourceFS
{
public:
AudioFileSourceFATFS() : AudioFileSourceFS(FFat) {};
AudioFileSourceFATFS(const char *filename) : AudioFileSourceFS(FFat) {
// We call open() ourselves because calling AudioFileSourceFS(FFat, filename)
// would call the parent open() and we do not want that
open(filename);
};
virtual bool open(const char *filename) override {
// make sure that the FATFS filesystem has been mounted
if (!FFat.begin()) {
audioLogger->printf_P(PSTR("Unable to initialize FATFS filesystem\n"));
return false;
} else {
// now that the fielsystem has been mounted, we can call the regular parent open() function
return AudioFileSourceFS::open(filename);
}
};
// Others are inherited from base
};
#endif
#endif

View File

@ -0,0 +1,73 @@
/*
AudioFileSourceFS
Input "file" to be used by AudioGenerator
Copyright (C) 2017 Earle F. Philhower, III
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "AudioFileSourceFS.h"
#ifdef ESP32
#include "SPIFFS.h"
#endif
AudioFileSourceFS::AudioFileSourceFS(FS &fs, const char *filename)
{
filesystem = &fs;
open(filename);
}
bool AudioFileSourceFS::open(const char *filename)
{
#ifndef ESP32
filesystem->begin();
#endif
f = filesystem->open(filename, "r");
return f;
}
AudioFileSourceFS::~AudioFileSourceFS()
{
if (f) f.close();
}
uint32_t AudioFileSourceFS::read(void *data, uint32_t len)
{
return f.read(reinterpret_cast<uint8_t*>(data), len);
}
bool AudioFileSourceFS::seek(int32_t pos, int dir)
{
return f.seek(pos, (dir==SEEK_SET)?SeekSet:(dir==SEEK_CUR)?SeekCur:SeekEnd);
}
bool AudioFileSourceFS::close()
{
f.close();
return true;
}
bool AudioFileSourceFS::isOpen()
{
return f?true:false;
}
uint32_t AudioFileSourceFS::getSize()
{
if (!f) return 0;
return f.size();
}

View File

@ -0,0 +1,51 @@
/*
AudioFileSourceFS
Input Arduion "file" to be used by AudioGenerator
Copyright (C) 2017 Earle F. Philhower, III
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef _AUDIOFILESOURCEFS_H
#define _AUDIOFILESOURCEFS_H
#include <Arduino.h>
#include <FS.h>
#include "AudioFileSource.h"
class AudioFileSourceFS : public AudioFileSource
{
public:
AudioFileSourceFS(FS &fs) { filesystem = &fs; }
AudioFileSourceFS(FS &fs, const char *filename);
virtual ~AudioFileSourceFS() override;
virtual bool open(const char *filename) override;
virtual uint32_t read(void *data, uint32_t len) override;
virtual bool seek(int32_t pos, int dir) override;
virtual bool close() override;
virtual bool isOpen() override;
virtual uint32_t getSize() override;
virtual uint32_t getPos() override { if (!f) return 0; else return f.position(); };
private:
FS *filesystem;
File f;
};
#endif

View File

@ -0,0 +1,154 @@
/*
AudioFileSourceHTTPStream
Streaming HTTP source
Copyright (C) 2017 Earle F. Philhower, III
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "AudioFileSourceHTTPStream.h"
AudioFileSourceHTTPStream::AudioFileSourceHTTPStream()
{
pos = 0;
reconnectTries = 0;
saveURL[0] = 0;
}
AudioFileSourceHTTPStream::AudioFileSourceHTTPStream(const char *url)
{
saveURL[0] = 0;
reconnectTries = 0;
open(url);
}
bool AudioFileSourceHTTPStream::open(const char *url)
{
pos = 0;
http.begin(client, url);
http.setReuse(true);
#ifndef ESP32
http.setFollowRedirects(true);
#endif
int code = http.GET();
if (code != HTTP_CODE_OK) {
http.end();
cb.st(STATUS_HTTPFAIL, PSTR("Can't open HTTP request"));
return false;
}
size = http.getSize();
strncpy(saveURL, url, sizeof(saveURL));
saveURL[sizeof(saveURL)-1] = 0;
return true;
}
AudioFileSourceHTTPStream::~AudioFileSourceHTTPStream()
{
http.end();
}
uint32_t AudioFileSourceHTTPStream::read(void *data, uint32_t len)
{
if (data==NULL) {
audioLogger->printf_P(PSTR("ERROR! AudioFileSourceHTTPStream::read passed NULL data\n"));
return 0;
}
return readInternal(data, len, false);
}
uint32_t AudioFileSourceHTTPStream::readNonBlock(void *data, uint32_t len)
{
if (data==NULL) {
audioLogger->printf_P(PSTR("ERROR! AudioFileSourceHTTPStream::readNonBlock passed NULL data\n"));
return 0;
}
return readInternal(data, len, true);
}
uint32_t AudioFileSourceHTTPStream::readInternal(void *data, uint32_t len, bool nonBlock)
{
retry:
if (!http.connected()) {
cb.st(STATUS_DISCONNECTED, PSTR("Stream disconnected"));
http.end();
for (int i = 0; i < reconnectTries; i++) {
char buff[32];
sprintf_P(buff, PSTR("Attempting to reconnect, try %d"), i);
cb.st(STATUS_RECONNECTING, buff);
delay(reconnectDelayMs);
if (open(saveURL)) {
cb.st(STATUS_RECONNECTED, PSTR("Stream reconnected"));
break;
}
}
if (!http.connected()) {
cb.st(STATUS_DISCONNECTED, PSTR("Unable to reconnect"));
return 0;
}
}
if ((size > 0) && (pos >= size)) return 0;
WiFiClient *stream = http.getStreamPtr();
// Can't read past EOF...
if ( (size > 0) && (len > (uint32_t)(pos - size)) ) len = pos - size;
if (!nonBlock) {
int start = millis();
while ((stream->available() < (int)len) && (millis() - start < 500)) yield();
}
size_t avail = stream->available();
if (!nonBlock && !avail) {
cb.st(STATUS_NODATA, PSTR("No stream data available"));
http.end();
goto retry;
}
if (avail == 0) return 0;
if (avail < len) len = avail;
int read = stream->read(reinterpret_cast<uint8_t*>(data), len);
pos += read;
return read;
}
bool AudioFileSourceHTTPStream::seek(int32_t pos, int dir)
{
audioLogger->printf_P(PSTR("ERROR! AudioFileSourceHTTPStream::seek not implemented!"));
(void) pos;
(void) dir;
return false;
}
bool AudioFileSourceHTTPStream::close()
{
http.end();
return true;
}
bool AudioFileSourceHTTPStream::isOpen()
{
return http.connected();
}
uint32_t AudioFileSourceHTTPStream::getSize()
{
return size;
}
uint32_t AudioFileSourceHTTPStream::getPos()
{
return pos;
}

View File

@ -0,0 +1,66 @@
/*
AudioFileSourceHTTPStream
Connect to a HTTP based streaming service
Copyright (C) 2017 Earle F. Philhower, III
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef _AUDIOFILESOURCEHTTPSTREAM_H
#define _AUDIOFILESOURCEHTTPSTREAM_H
#include <Arduino.h>
#ifdef ESP32
#include <HTTPClient.h>
#else
#include <WiFiClient.h>
#include <ESP8266HTTPClient.h>
#endif
#include "AudioFileSource.h"
class AudioFileSourceHTTPStream : public AudioFileSource
{
friend class AudioFileSourceICYStream;
public:
AudioFileSourceHTTPStream();
AudioFileSourceHTTPStream(const char *url);
virtual ~AudioFileSourceHTTPStream() override;
virtual bool open(const char *url) override;
virtual uint32_t read(void *data, uint32_t len) override;
virtual uint32_t readNonBlock(void *data, uint32_t len) override;
virtual bool seek(int32_t pos, int dir) override;
virtual bool close() override;
virtual bool isOpen() override;
virtual uint32_t getSize() override;
virtual uint32_t getPos() override;
bool SetReconnect(int tries, int delayms) { reconnectTries = tries; reconnectDelayMs = delayms; return true; }
enum { STATUS_HTTPFAIL=2, STATUS_DISCONNECTED, STATUS_RECONNECTING, STATUS_RECONNECTED, STATUS_NODATA };
private:
virtual uint32_t readInternal(void *data, uint32_t len, bool nonBlock);
WiFiClient client;
HTTPClient http;
int pos;
int size;
int reconnectTries;
int reconnectDelayMs;
char saveURL[128];
};
#endif

View File

@ -0,0 +1,213 @@
/*
AudioFileSourceICYStream
Streaming Shoutcast ICY source
Copyright (C) 2017 Earle F. Philhower, III
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#define _GNU_SOURCE
#include "AudioFileSourceICYStream.h"
#include <string.h>
AudioFileSourceICYStream::AudioFileSourceICYStream()
{
pos = 0;
reconnectTries = 0;
saveURL[0] = 0;
}
AudioFileSourceICYStream::AudioFileSourceICYStream(const char *url)
{
saveURL[0] = 0;
reconnectTries = 0;
open(url);
}
bool AudioFileSourceICYStream::open(const char *url)
{
static const char *hdr[] = { "icy-metaint", "icy-name", "icy-genre", "icy-br" };
pos = 0;
http.begin(client, url);
http.addHeader("Icy-MetaData", "1");
http.collectHeaders( hdr, 4 );
http.setReuse(true);
int code = http.GET();
if (code != HTTP_CODE_OK) {
http.end();
cb.st(STATUS_HTTPFAIL, PSTR("Can't open HTTP request"));
return false;
}
if (http.hasHeader(hdr[0])) {
String ret = http.header(hdr[0]);
icyMetaInt = ret.toInt();
} else {
icyMetaInt = 0;
}
if (http.hasHeader(hdr[1])) {
String ret = http.header(hdr[1]);
// cb.md("SiteName", false, ret.c_str());
}
if (http.hasHeader(hdr[2])) {
String ret = http.header(hdr[2]);
// cb.md("Genre", false, ret.c_str());
}
if (http.hasHeader(hdr[3])) {
String ret = http.header(hdr[3]);
// cb.md("Bitrate", false, ret.c_str());
}
icyByteCount = 0;
size = http.getSize();
strncpy(saveURL, url, sizeof(saveURL));
saveURL[sizeof(saveURL)-1] = 0;
return true;
}
AudioFileSourceICYStream::~AudioFileSourceICYStream()
{
http.end();
}
uint32_t AudioFileSourceICYStream::readInternal(void *data, uint32_t len, bool nonBlock)
{
retry:
if (!http.connected()) {
cb.st(STATUS_DISCONNECTED, PSTR("Stream disconnected"));
http.end();
for (int i = 0; i < reconnectTries; i++) {
char buff[32];
sprintf_P(buff, PSTR("Attempting to reconnect, try %d"), i);
cb.st(STATUS_RECONNECTING, buff);
delay(reconnectDelayMs);
if (open(saveURL)) {
cb.st(STATUS_RECONNECTED, PSTR("Stream reconnected"));
break;
}
}
if (!http.connected()) {
cb.st(STATUS_DISCONNECTED, PSTR("Unable to reconnect"));
return 0;
}
}
if ((size > 0) && (pos >= size)) return 0;
WiFiClient *stream = http.getStreamPtr();
// Can't read past EOF...
if ( (size > 0) && (len > (uint32_t)(pos - size)) ) len = pos - size;
if (!nonBlock) {
int start = millis();
while ((stream->available() < (int)len) && (millis() - start < 500)) yield();
}
size_t avail = stream->available();
if (!nonBlock && !avail) {
cb.st(STATUS_NODATA, PSTR("No stream data available"));
http.end();
goto retry;
}
if (avail == 0) return 0;
if (avail < len) len = avail;
int read = 0;
int ret = 0;
// If the read would hit an ICY block, split it up...
if (((int)(icyByteCount + len) > (int)icyMetaInt) && (icyMetaInt > 0)) {
int beforeIcy = icyMetaInt - icyByteCount;
if (beforeIcy > 0) {
ret = stream->read(reinterpret_cast<uint8_t*>(data), beforeIcy);
if (ret < 0) ret = 0;
read += ret;
pos += ret;
len -= ret;
data = (void *)(reinterpret_cast<char*>(data) + ret);
icyByteCount += ret;
if (ret != beforeIcy) return read; // Partial read
}
// ICY MD handling
int mdSize;
uint8_t c;
int mdret = stream->read(&c, 1);
if (mdret==0) return read;
mdSize = c * 16;
if ((mdret == 1) && (mdSize > 0)) {
// This is going to get ugly fast.
char icyBuff[256 + 16 + 1];
char *readInto = icyBuff + 16;
memset(icyBuff, 0, 16); // Ensure no residual matches occur
while (mdSize) {
int toRead = mdSize > 256 ? 256 : mdSize;
int ret = stream->read((uint8_t*)readInto, toRead);
if (ret < 0) return read;
if (ret == 0) { delay(1); continue; }
mdSize -= ret;
// At this point we have 0...15 = last 15 chars read from prior read plus new data
int end = 16 + ret; // The last byte of valid data
char *header = (char *)memmem((void*)icyBuff, end, (void*)"StreamTitle=", 12);
if (!header) {
// No match, so move the last 16 bytes back to the start and continue
memmove(icyBuff, icyBuff+end-16, 16);
delay(1);
continue;
}
// Found header, now move it to the front
int lastValidByte = end - (header -icyBuff) + 1;
memmove(icyBuff, header, lastValidByte);
// Now fill the buffer to the end with read data
while (mdSize && lastValidByte < 255) {
int toRead = mdSize > (256 - lastValidByte) ? (256 - lastValidByte) : mdSize;
ret = stream->read((uint8_t*)icyBuff + lastValidByte, toRead);
if (ret==-1) return read; // error
if (ret == 0) { delay(1); continue; }
mdSize -= ret;
lastValidByte += ret;
}
// Buffer now contains StreamTitle=....., parse it
char *p = icyBuff+12;
if (*p=='\'' || *p== '"' ) {
char closing[] = { *p, ';', '\0' };
char *psz = strstr( p+1, closing );
if( !psz ) psz = strchr( &icyBuff[13], ';' );
if( psz ) *psz = '\0';
p++;
} else {
char *psz = strchr( p, ';' );
if( psz ) *psz = '\0';
}
cb.md("StreamTitle", false, p);
// Now skip rest of MD block
while (mdSize) {
int toRead = mdSize > 256 ? 256 : mdSize;
ret = stream->read((uint8_t*)icyBuff, toRead);
if (ret < 0) return read;
if (ret == 0) { delay(1); continue; }
mdSize -= ret;
}
}
}
icyByteCount = 0;
}
ret = stream->read(reinterpret_cast<uint8_t*>(data), len);
if (ret < 0) ret = 0;
read += ret;
pos += ret;
icyByteCount += ret;
return read;
}

View File

@ -0,0 +1,49 @@
/*
AudioFileSourceHTTPStream
Connect to a HTTP based streaming service
Copyright (C) 2017 Earle F. Philhower, III
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef _AUDIOFILESOURCEICYSTREAM_H
#define _AUDIOFILESOURCEICYSTREAM_H
#include <Arduino.h>
#ifdef ESP32
#include <HTTPClient.h>
#else
#include <ESP8266HTTPClient.h>
#endif
#include "AudioFileSourceHTTPStream.h"
class AudioFileSourceICYStream : public AudioFileSourceHTTPStream
{
public:
AudioFileSourceICYStream();
AudioFileSourceICYStream(const char *url);
virtual ~AudioFileSourceICYStream() override;
virtual bool open(const char *url) override;
private:
virtual uint32_t readInternal(void *data, uint32_t len, bool nonBlock) override;
int icyMetaInt;
int icyByteCount;
};
#endif

View File

@ -0,0 +1,265 @@
/*
AudioFileSourceID3
ID3 filter that extracts any ID3 fields and sends to CB function
Copyright (C) 2017 Earle F. Philhower, III
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "AudioFileSourceID3.h"
// Handle unsync operation in ID3 with custom class
class AudioFileSourceUnsync : public AudioFileSource
{
public:
AudioFileSourceUnsync(AudioFileSource *src, int len, bool unsync);
virtual ~AudioFileSourceUnsync() override;
virtual uint32_t read(void *data, uint32_t len) override;
int getByte();
bool eof();
private:
AudioFileSource *src;
int remaining;
bool unsync;
int savedByte;
};
AudioFileSourceUnsync::AudioFileSourceUnsync(AudioFileSource *src, int len, bool unsync)
{
this->src = src;
this->remaining = len;
this->unsync = unsync;
this->savedByte = -1;
}
AudioFileSourceUnsync::~AudioFileSourceUnsync()
{
}
uint32_t AudioFileSourceUnsync::read(void *data, uint32_t len)
{
uint32_t bytes = 0;
uint8_t *ptr = reinterpret_cast<uint8_t*>(data);
// This is only used during ID3 parsing, so no need to optimize here...
while (len--) {
int b = getByte();
if (b >= 0) {
*(ptr++) = (uint8_t)b;
bytes++;
}
}
return bytes;
}
int AudioFileSourceUnsync::getByte()
{
// If we're not unsync, just read.
if (!unsync) {
uint8_t c;
if (!remaining) return -1;
remaining--;
if (1 != src->read(&c, 1)) return -1;
return c;
}
// If we've saved a pre-read character, return it immediately
if (savedByte >= 0) {
int s = savedByte;
savedByte = -1;
return s;
}
if (remaining <= 0) {
return -1;
} else if (remaining == 1) {
remaining--;
uint8_t c;
if (1 != src->read(&c, 1)) return -1;
else return c;
} else {
uint8_t c;
remaining--;
if (1 != src->read(&c, 1)) return -1;
if (c != 0xff) {
return c;
}
// Saw 0xff, check next byte. If 0 then eat it, OTW return the 0xff
uint8_t d;
remaining--;
if (1 != src->read(&d, 1)) return c;
if (d != 0x00) {
savedByte = d;
}
return c;
}
}
bool AudioFileSourceUnsync::eof()
{
if (remaining<=0) return true;
else return false;
}
AudioFileSourceID3::AudioFileSourceID3(AudioFileSource *src)
{
this->src = src;
this->checked = false;
}
AudioFileSourceID3::~AudioFileSourceID3()
{
}
uint32_t AudioFileSourceID3::read(void *data, uint32_t len)
{
int rev = 0;
if (checked) {
return src->read(data, len);
}
checked = true;
// <10 bytes initial read, not enough space to check header
if (len<10) return src->read(data, len);
uint8_t *buff = reinterpret_cast<uint8_t*>(data);
int ret = src->read(data, 10);
if (ret<10) return ret;
if ((buff[0]!='I') || (buff[1]!='D') || (buff[2]!='3') || (buff[3]>0x04) || (buff[3]<0x02) || (buff[4]!=0)) {
return 10 + src->read(buff+10, len-10);
}
rev = buff[3];
bool unsync = false;
bool exthdr = false;
switch(rev) {
case 2:
unsync = (buff[5] & 0x80);
exthdr = false;
break;
case 3:
case 4:
unsync = (buff[5] & 0x80);
exthdr = (buff[5] & 0x40);
break;
};
int id3Size = buff[6];
id3Size = id3Size << 7;
id3Size |= buff[7];
id3Size = id3Size << 7;
id3Size |= buff[8];
id3Size = id3Size << 7;
id3Size |= buff[9];
// Every read from now may be unsync'd
AudioFileSourceUnsync id3(src, id3Size, unsync);
if (exthdr) {
int ehsz = (id3.getByte()<<24) | (id3.getByte()<<16) | (id3.getByte()<<8) | (id3.getByte());
for (int j=0; j<ehsz-4; j++) id3.getByte(); // Throw it away
}
do {
unsigned char frameid[4];
int framesize;
bool compressed;
frameid[0] = id3.getByte();
frameid[1] = id3.getByte();
frameid[2] = id3.getByte();
if (rev==2) frameid[3] = 0;
else frameid[3] = id3.getByte();
if (frameid[0]==0 && frameid[1]==0 && frameid[2]==0 && frameid[3]==0) {
// We're in padding
while (!id3.eof()) {
id3.getByte();
}
} else {
if (rev==2) {
framesize = (id3.getByte()<<16) | (id3.getByte()<<8) | (id3.getByte());
compressed = false;
} else {
framesize = (id3.getByte()<<24) | (id3.getByte()<<16) | (id3.getByte()<<8) | (id3.getByte());
id3.getByte(); // skip 1st flag
compressed = id3.getByte()&0x80;
}
if (compressed) {
int decompsize = (id3.getByte()<<24) | (id3.getByte()<<16) | (id3.getByte()<<8) | (id3.getByte());
// TODO - add libz decompression, for now ignore this one...
(void)decompsize;
for (int j=0; j<framesize; j++)
id3.getByte();
}
// Read the value and send to callback
char value[64];
uint16_t i;
bool isUnicode = (id3.getByte()==1) ? true : false;
for (i=0; i<framesize-1; i++) {
if (i<sizeof(value)-1) value[i] = id3.getByte();
else (void)id3.getByte();
}
value[i<sizeof(value)-1?i:sizeof(value)-1] = 0; // Terminate the string...
if ( (frameid[0]=='T' && frameid[1]=='A' && frameid[2]=='L' && frameid[3] == 'B' ) ||
(frameid[0]=='T' && frameid[1]=='A' && frameid[2]=='L' && rev==2) ) {
cb.md("Album", isUnicode, value);
} else if ( (frameid[0]=='T' && frameid[1]=='I' && frameid[2]=='T' && frameid[3] == '2') ||
(frameid[0]=='T' && frameid[1]=='T' && frameid[2]=='2' && rev==2) ) {
cb.md("Title", isUnicode, value);
} else if ( (frameid[0]=='T' && frameid[1]=='P' && frameid[2]=='E' && frameid[3] == '1') ||
(frameid[0]=='T' && frameid[1]=='P' && frameid[2]=='1' && rev==2) ) {
cb.md("Performer", isUnicode, value);
} else if ( (frameid[0]=='T' && frameid[1]=='Y' && frameid[2]=='E' && frameid[3] == 'R') ||
(frameid[0]=='T' && frameid[1]=='Y' && frameid[2]=='E' && rev==2) ) {
cb.md("Year", isUnicode, value);
}
}
} while (!id3.eof());
// All ID3 processing done, return to main caller
return src->read(data, len);
}
bool AudioFileSourceID3::seek(int32_t pos, int dir)
{
return src->seek(pos, dir);
}
bool AudioFileSourceID3::close()
{
return src->close();
}
bool AudioFileSourceID3::isOpen()
{
return src->isOpen();
}
uint32_t AudioFileSourceID3::getSize()
{
return src->getSize();
}
uint32_t AudioFileSourceID3::getPos()
{
return src->getPos();
}

View File

@ -0,0 +1,48 @@
/*
AudioFileSourceID3
ID3 filter that extracts any ID3 fields and sends to CB function
Copyright (C) 2017 Earle F. Philhower, III
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef _AUDIOFILESOURCEID3_H
#define _AUDIOFILESOURCEID3_H
#include <Arduino.h>
#include "AudioFileSource.h"
class AudioFileSourceID3 : public AudioFileSource
{
public:
AudioFileSourceID3(AudioFileSource *src);
virtual ~AudioFileSourceID3() override;
virtual uint32_t read(void *data, uint32_t len) override;
virtual bool seek(int32_t pos, int dir) override;
virtual bool close() override;
virtual bool isOpen() override;
virtual uint32_t getSize() override;
virtual uint32_t getPos() override;
private:
AudioFileSource *src;
bool checked;
};
#endif

View File

@ -0,0 +1,43 @@
/*
AudioFileSourceFS
Input Arduion "file" to be used by AudioGenerator
Copyright (C) 2017 Earle F. Philhower, III
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef _AUDIOFILESOURCESPIFFS_H
#define _AUDIOFILESOURCESPIFFS_H
#ifndef ESP32 // No LittleFS there, yet
#include <Arduino.h>
#include <LittleFS.h>
#include "AudioFileSource.h"
#include "AudioFileSourceFS.h"
class AudioFileSourceLittleFS : public AudioFileSourceFS
{
public:
AudioFileSourceLittleFS() : AudioFileSourceFS(LittleFS) { };
AudioFileSourceLittleFS(const char *filename) : AudioFileSourceFS(LittleFS, filename) {};
// Others are inherited from base
};
#endif
#endif

View File

@ -0,0 +1,99 @@
/*
AudioFileSourcePROGMEM
Store a "file" as a PROGMEM array and use it as audio source data
Copyright (C) 2017 Earle F. Philhower, III
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "AudioFileSourcePROGMEM.h"
AudioFileSourcePROGMEM::AudioFileSourcePROGMEM()
{
opened = false;
progmemData = NULL;
progmemLen = 0;
filePointer = 0;
}
AudioFileSourcePROGMEM::AudioFileSourcePROGMEM(const void *data, uint32_t len)
{
open(data, len);
}
AudioFileSourcePROGMEM::~AudioFileSourcePROGMEM()
{
}
bool AudioFileSourcePROGMEM::open(const void *data, uint32_t len)
{
if (!data || !len) return false;
opened = true;
progmemData = data;
progmemLen = len;
filePointer = 0;
return true;
}
uint32_t AudioFileSourcePROGMEM::getSize()
{
if (!opened) return 0;
return progmemLen;
}
bool AudioFileSourcePROGMEM::isOpen()
{
return opened;
}
bool AudioFileSourcePROGMEM::close()
{
opened = false;
progmemData = NULL;
progmemLen = 0;
filePointer = 0;
return true;
}
bool AudioFileSourcePROGMEM::seek(int32_t pos, int dir)
{
if (!opened) return false;
uint32_t newPtr;
switch (dir) {
case SEEK_SET: newPtr = pos; break;
case SEEK_CUR: newPtr = filePointer + pos; break;
case SEEK_END: newPtr = progmemLen - pos; break;
default: return false;
}
if (newPtr > progmemLen) return false;
filePointer = newPtr;
return true;
}
uint32_t AudioFileSourcePROGMEM::read(void *data, uint32_t len)
{
if (!opened) return 0;
if (filePointer >= progmemLen) return 0;
uint32_t toRead = progmemLen - filePointer;
if (toRead > len) toRead = len;
memcpy_P(data, reinterpret_cast<const uint8_t*>(progmemData)+filePointer, toRead);
filePointer += toRead;
return toRead;
}

View File

@ -0,0 +1,49 @@
/*
AudioFileSourcePROGMEM
Store a "file" as a PROGMEM array and use it as audio source data
Copyright (C) 2017 Earle F. Philhower, III
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef _AUDIOFILESOURCEPROGMEM_H
#define _AUDIOFILESOURCEPROGMEM_H
#include "AudioFileSource.h"
class AudioFileSourcePROGMEM : public AudioFileSource
{
public:
AudioFileSourcePROGMEM();
AudioFileSourcePROGMEM(const void *data, uint32_t len);
virtual ~AudioFileSourcePROGMEM() override;
virtual uint32_t read(void *data, uint32_t len) override;
virtual bool seek(int32_t pos, int dir) override;
virtual bool close() override;
virtual bool isOpen() override;
virtual uint32_t getSize() override;
virtual uint32_t getPos() override { if (!opened) return 0; else return filePointer; };
bool open(const void *data, uint32_t len);
private:
bool opened;
const void *progmemData;
uint32_t progmemLen;
uint32_t filePointer;
};
#endif

View File

@ -0,0 +1,78 @@
/*
AudioFileSourceSPIFFS
Input SD card "file" to be used by AudioGenerator
Copyright (C) 2017 Earle F. Philhower, III
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "AudioFileSourceSD.h"
AudioFileSourceSD::AudioFileSourceSD()
{
}
AudioFileSourceSD::AudioFileSourceSD(const char *filename)
{
open(filename);
}
bool AudioFileSourceSD::open(const char *filename)
{
f = SD.open(filename, FILE_READ);
return f;
}
AudioFileSourceSD::~AudioFileSourceSD()
{
if (f) f.close();
}
uint32_t AudioFileSourceSD::read(void *data, uint32_t len)
{
return f.read(reinterpret_cast<uint8_t*>(data), len);
}
bool AudioFileSourceSD::seek(int32_t pos, int dir)
{
if (!f) return false;
if (dir==SEEK_SET) return f.seek(pos);
else if (dir==SEEK_CUR) return f.seek(f.position() + pos);
else if (dir==SEEK_END) return f.seek(f.size() + pos);
return false;
}
bool AudioFileSourceSD::close()
{
f.close();
return true;
}
bool AudioFileSourceSD::isOpen()
{
return f?true:false;
}
uint32_t AudioFileSourceSD::getSize()
{
if (!f) return 0;
return f.size();
}
uint32_t AudioFileSourceSD::getPos()
{
if (!f) return 0;
return f.position();
}

View File

@ -0,0 +1,52 @@
/*
AudioFileSourceSPIFFS
Input SD card "file" to be used by AudioGenerator
Copyright (C) 2017 Earle F. Philhower, III
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef _AUDIOFILESOURCESD_H
#define _AUDIOFILESOURCESD_H
#include "AudioFileSource.h"
#ifdef ESP8266
#include <SdFat.h>
#include <SDFS.h>
#endif
#include <SD.h>
class AudioFileSourceSD : public AudioFileSource
{
public:
AudioFileSourceSD();
AudioFileSourceSD(const char *filename);
virtual ~AudioFileSourceSD() override;
virtual bool open(const char *filename) override;
virtual uint32_t read(void *data, uint32_t len) override;
virtual bool seek(int32_t pos, int dir) override;
virtual bool close() override;
virtual bool isOpen() override;
virtual uint32_t getSize() override;
virtual uint32_t getPos() override;
private:
File f;
};
#endif

View File

@ -0,0 +1,40 @@
/*
AudioFileSourceFS
Input Arduion "file" to be used by AudioGenerator
Copyright (C) 2017 Earle F. Philhower, III
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef _AUDIOFILESOURCESPIFFS_H
#define _AUDIOFILESOURCESPIFFS_H
#include <Arduino.h>
#include <FS.h>
#include "AudioFileSource.h"
#include "AudioFileSourceFS.h"
class AudioFileSourceSPIFFS : public AudioFileSourceFS
{
public:
AudioFileSourceSPIFFS() : AudioFileSourceFS(SPIFFS) { };
AudioFileSourceSPIFFS(const char *filename) : AudioFileSourceFS(SPIFFS, filename) {};
// Others are inherited from base
};
#endif

View File

@ -0,0 +1,167 @@
/*
AudioFileSourceSPIRAMBuffer
Buffered file source in external SPI RAM
Copyright (C) 2017 Sebastien Decourriere
Based on AudioFileSourceBuffer class from Earle F. Philhower, III
Copyright (C) 2020 Earle F. Philhower, III
Rewritten for speed and functionality
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <Arduino.h>
#include "AudioFileSourceSPIRAMBuffer.h"
#pragma GCC optimize ("O3")
AudioFileSourceSPIRAMBuffer::AudioFileSourceSPIRAMBuffer(AudioFileSource *source, uint8_t csPin, uint32_t buffSizeBytes)
{
ram.begin(40, csPin);
ramSize = buffSizeBytes;
writePtr = 0;
readPtr = 0;
filled = false;
src = source;
audioLogger->printf_P(PSTR("SPI RAM buffer size: %u Bytes\n"), ramSize);
}
AudioFileSourceSPIRAMBuffer::~AudioFileSourceSPIRAMBuffer()
{
ram.end();
}
bool AudioFileSourceSPIRAMBuffer::seek(int32_t pos, int dir)
{
// Invalidate
readPtr = 0;
writePtr = 0;
filled = false;
return src->seek(pos, dir);
}
bool AudioFileSourceSPIRAMBuffer::close()
{
return src->close();
}
bool AudioFileSourceSPIRAMBuffer::isOpen()
{
return src->isOpen();
}
uint32_t AudioFileSourceSPIRAMBuffer::getSize()
{
return src->getSize();
}
uint32_t AudioFileSourceSPIRAMBuffer::getPos()
{
return src->getPos() - (writePtr - readPtr);
}
uint32_t AudioFileSourceSPIRAMBuffer::read(void *data, uint32_t len)
{
uint32_t bytes = 0;
// Check if the buffer isn't empty, otherwise we try to fill completely
if (!filled) {
cb.st(999, PSTR("Filling buffer..."));
uint8_t buffer[256];
writePtr = 0;
readPtr = 0;
// Fill up completely before returning any data at all
do {
int toRead = std::min(ramSize - (writePtr - readPtr), sizeof(buffer));
int length = src->read(buffer, toRead);
if (length > 0) {
#ifdef FAKERAM
for (size_t i=0; i<length; i++) fakeRAM[(i+writePtr)%ramSize] = buffer[i];
#else
ram.writeBytes(writePtr % ramSize, buffer, length);
#endif
writePtr += length;
} else {
// EOF, break out of read loop
break;
}
} while ((writePtr - readPtr) < ramSize);
filled = true;
cb.st(999, PSTR("Buffer filled..."));
}
// Read up to the entire buffer from RAM
uint32_t toReadFromBuffer = std::min(len, writePtr - readPtr);
uint8_t *ptr = reinterpret_cast<uint8_t*>(data);
if (toReadFromBuffer > 0) {
#ifdef FAKERAM
for (size_t i=0; i<toReadFromBuffer; i++) ptr[i] = fakeRAM[(i+readPtr)%ramSize];
#else
ram.readBytes(readPtr % ramSize, ptr, toReadFromBuffer);
#endif
readPtr += toReadFromBuffer;
ptr += toReadFromBuffer;
bytes += toReadFromBuffer;
len -= toReadFromBuffer;
}
// If len>0 there is no data left in buffer and we try to read more directly from source.
// Then, we trigger a complete buffer refill
if (len) {
bytes += src->read(data, len);
filled = false;
}
return bytes;
}
void AudioFileSourceSPIRAMBuffer::fill()
{
// Make sure the buffer is pre-filled before make partial fill.
if (!filled) return;
for (auto i=0; i<5; i++) {
// Make sure there is at least buffer size free in RAM
uint8_t buffer[128];
if ((ramSize - (writePtr - readPtr)) < sizeof(buffer)) {
return;
}
int cnt = src->readNonBlock(buffer, sizeof(buffer));
if (cnt) {
#ifdef FAKERAM
for (size_t i=0; i<cnt; i++) fakeRAM[(i+writePtr)%ramSize] = buffer[i];
#else
ram.writeBytes(writePtr % ramSize, buffer, cnt);
#endif
writePtr += cnt;
}
}
}
bool AudioFileSourceSPIRAMBuffer::loop()
{
static uint32_t last = 0;
if (!src->loop()) return false;
fill();
if ((ESP.getCycleCount() - last) > microsecondsToClockCycles(1000000)) {
last = ESP.getCycleCount();
char str[65];
memset(str, '#', 64);
str[64] = 0;
str[((writePtr - readPtr) * 64)/ramSize] = 0;
cb.st(((writePtr - readPtr) * 100)/ramSize, str);
}
return true;
}

View File

@ -0,0 +1,65 @@
/*
AudioFileSourceSPIRAMBuffer
Buffered file source in external SPI RAM
Copyright (C) 2017 Sebastien Decourriere
Based on AudioFileSourceBuffer class from Earle F. Philhower, III
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef _AUDIOFILESOURCESPIRAMBUFFER_H
#define _AUDIOFILESOURCESPIRAMBUFFER_H
#include "AudioFileSource.h"
#include <SPI.h>
#include "spiram-fast.h"
//#define FAKERAM
// #define SPIBUF_DEBUG
class AudioFileSourceSPIRAMBuffer : public AudioFileSource
{
public:
#ifdef FAKERAM
AudioFileSourceSPIRAMBuffer(AudioFileSource *in, uint8_t csPin = 15, uint32_t bufferBytes = 2048);
#else
AudioFileSourceSPIRAMBuffer(AudioFileSource *in, uint8_t csPin = 15, uint32_t bufferBytes = 128*1024);
#endif
virtual ~AudioFileSourceSPIRAMBuffer() override;
virtual uint32_t read(void *data, uint32_t len) override;
virtual bool seek(int32_t pos, int dir) override;
virtual bool close() override;
virtual bool isOpen() override;
virtual uint32_t getSize() override;
virtual uint32_t getPos() override;
virtual bool loop() override;
private:
virtual void fill();
private:
AudioFileSource *src;
ESP8266SPIRAM ram;
size_t ramSize;
size_t writePtr;
size_t readPtr;
bool filled;
#ifdef FAKERAM
char fakeRAM[2048];
#endif
};
#endif

View File

@ -0,0 +1,98 @@
/*
AudioFileSourceSTDIO
Input STDIO "file" to be used by AudioGenerator
Only for hot-based testing
Copyright (C) 2017 Earle F. Philhower, III
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <Arduino.h>
#ifndef ARDUINO
#include <time.h>
#include "AudioFileSourceSTDIO.h"
AudioFileSourceSTDIO::AudioFileSourceSTDIO()
{
f = NULL;
srand(time(NULL));
}
AudioFileSourceSTDIO::AudioFileSourceSTDIO(const char *filename)
{
open(filename);
}
bool AudioFileSourceSTDIO::open(const char *filename)
{
f = fopen(filename, "rb");
return f;
}
AudioFileSourceSTDIO::~AudioFileSourceSTDIO()
{
if (f) fclose(f);
f = NULL;
}
uint32_t AudioFileSourceSTDIO::read(void *data, uint32_t len)
{
// if (rand() % 100 == 69) { // Give 0 data 1%
// printf("0 read\n");
// len = 0;
// } else if (rand() % 100 == 1) { // Give short reads 1%
// printf("0 read\n");
// len = 0;
// }
int ret = fread(reinterpret_cast<uint8_t*>(data), 1, len, f);
// if (ret && rand() % 100 < 5 ) {
// // We're really mean...throw bad data in the mix
// printf("bad data\n");
// for (int i=0; i<100; i++)
// *(reinterpret_cast<uint8_t*>(data) + (rand() % ret)) = rand();
// }
return ret;
}
bool AudioFileSourceSTDIO::seek(int32_t pos, int dir)
{
return fseek(f, pos, dir) == 0;
}
bool AudioFileSourceSTDIO::close()
{
fclose(f);
f = NULL;
return true;
}
bool AudioFileSourceSTDIO::isOpen()
{
return f?true:false;
}
uint32_t AudioFileSourceSTDIO::getSize()
{
if (!f) return 0;
uint32_t p = ftell(f);
fseek(f, 0, SEEK_END);
uint32_t len = ftell(f);
fseek(f, p, SEEK_SET);
return len;
}
#endif

View File

@ -0,0 +1,53 @@
/*
AudioFileSourceSTDIO
Input SPIFFS "file" to be used by AudioGenerator
Only for host-based testing, not Arduino
Copyright (C) 2017 Earle F. Philhower, III
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef _AUDIOFILESOURCESTDIO_H
#define _AUDIOFILESOURCESTDIO_H
#include <Arduino.h>
#ifndef ARDUINO
#include "AudioFileSource.h"
class AudioFileSourceSTDIO : public AudioFileSource
{
public:
AudioFileSourceSTDIO();
AudioFileSourceSTDIO(const char *filename);
virtual ~AudioFileSourceSTDIO() override;
virtual bool open(const char *filename) override;
virtual uint32_t read(void *data, uint32_t len) override;
virtual bool seek(int32_t pos, int dir) override;
virtual bool close() override;
virtual bool isOpen() override;
virtual uint32_t getSize() override;
virtual uint32_t getPos() override { if (!f) return 0; else return (uint32_t)ftell(f); };
private:
FILE *f;
};
#endif // !ARDUINO
#endif

View File

@ -0,0 +1,61 @@
#include <Arduino.h>
#include "AudioFileStream.h"
AudioFileStream::AudioFileStream(AudioFileSource *source, int definedLen)
{
src = source;
len = definedLen;
ptr = 0;
saved = -1;
}
AudioFileStream::~AudioFileStream()
{
// If there's a defined len, read until we're empty
if (len) {
while (ptr++ < len) (void)read();
}
}
int AudioFileStream::available()
{
if (saved >= 0) return 1;
else if (len) return ptr - len;
else if (src->getSize()) return (src->getPos() - src->getSize());
else return 1;
}
int AudioFileStream::read()
{
uint8_t c;
int r;
if (ptr >= len) return -1;
ptr++;
if (saved >= 0) {
c = (uint8_t)saved;
saved = -1;
r = 1;
} else {
r = src->read(&c, 1);
}
if (r != 1) return -1;
return (int)c;
}
int AudioFileStream::peek()
{
uint8_t c;
if ((ptr+1) >= len) return -1;
if (saved >= 0) return saved;
int r = src->read(&c, 1);
if (r<1) return -1;
saved = c;
return saved;
}
void AudioFileStream::flush()
{
/* noop? */
}

View File

@ -0,0 +1,48 @@
/*
AudioFileStream
Convert an AudioFileSource* to a Stream*
Copyright (C) 2017 Earle F. Philhower, III
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef AUDIOFILESTREAM_H
#define AUDIOFILESTREAM_H
#include <Arduino.h>
#include "AudioFileSource.h"
class AudioFileStream : public Stream
{
public:
AudioFileStream(AudioFileSource *source, int definedLen);
virtual ~AudioFileStream();
public:
// Stream interface - see the Arduino library documentation.
virtual int available() override;
virtual int read() override;
virtual int peek() override;
virtual void flush() override;
virtual size_t write(uint8_t x) override { (void)x; return 0; };
private:
AudioFileSource *src;
int saved;
int len;
int ptr;
};
#endif

View File

@ -0,0 +1,53 @@
/*
AudioGenerator
Base class of an audio output generator
Copyright (C) 2017 Earle F. Philhower, III
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef _AUDIOGENERATOR_H
#define _AUDIOGENERATOR_H
#include <Arduino.h>
#include "AudioStatus.h"
#include "AudioFileSource.h"
#include "AudioOutput.h"
class AudioGenerator
{
public:
AudioGenerator() { lastSample[0] = 0; lastSample[1] = 0; };
virtual ~AudioGenerator() {};
virtual bool begin(AudioFileSource *source, AudioOutput *output) { (void)source; (void)output; return false; };
virtual bool loop() { return false; };
virtual bool stop() { return false; };
virtual bool isRunning() { return false;};
public:
virtual bool RegisterMetadataCB(AudioStatus::metadataCBFn fn, void *data) { return cb.RegisterMetadataCB(fn, data); }
virtual bool RegisterStatusCB(AudioStatus::statusCBFn fn, void *data) { return cb.RegisterStatusCB(fn, data); }
protected:
bool running;
AudioFileSource *file;
AudioOutput *output;
int16_t lastSample[2];
protected:
AudioStatus cb;
};
#endif

View File

@ -0,0 +1,214 @@
/*
AudioGeneratorAAC
Audio output generator using the Helix AAC decoder
Copyright (C) 2017 Earle F. Philhower, III
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma GCC optimize ("O3")
#include "AudioGeneratorAAC.h"
AudioGeneratorAAC::AudioGeneratorAAC()
{
preallocateSpace = NULL;
preallocateSize = 0;
running = false;
file = NULL;
output = NULL;
buff = (uint8_t*)malloc(buffLen);
outSample = (int16_t*)malloc(1024 * 2 * sizeof(uint16_t));
if (!buff || !outSample) {
audioLogger->printf_P(PSTR("ERROR: Out of memory in AAC\n"));
Serial.flush();
}
hAACDecoder = AACInitDecoder();
if (!hAACDecoder) {
audioLogger->printf_P(PSTR("Out of memory error! hAACDecoder==NULL\n"));
Serial.flush();
}
buffValid = 0;
lastFrameEnd = 0;
validSamples = 0;
curSample = 0;
lastRate = 0;
lastChannels = 0;
}
AudioGeneratorAAC::AudioGeneratorAAC(void *preallocateData, int preallocateSz)
{
preallocateSpace = preallocateData;
preallocateSize = preallocateSz;
running = false;
file = NULL;
output = NULL;
uint8_t *p = (uint8_t*)preallocateSpace;
buff = (uint8_t*) p;
p += (buffLen + 7) & ~7;
outSample = (int16_t*) p;
p += (1024 * 2 * sizeof(int16_t) + 7) & ~7;
int used = p - (uint8_t*)preallocateSpace;
int availSpace = preallocateSize - used;
if (availSpace < 0 ) {
audioLogger->printf_P(PSTR("ERROR: Out of memory in AAC\n"));
}
hAACDecoder = AACInitDecoderPre(p, availSpace);
if (!hAACDecoder) {
audioLogger->printf_P(PSTR("Out of memory error! hAACDecoder==NULL\n"));
Serial.flush();
}
buffValid = 0;
lastFrameEnd = 0;
validSamples = 0;
curSample = 0;
lastRate = 0;
lastChannels = 0;
}
AudioGeneratorAAC::~AudioGeneratorAAC()
{
if (!preallocateSpace) {
AACFreeDecoder(hAACDecoder);
free(buff);
free(outSample);
}
}
bool AudioGeneratorAAC::stop()
{
running = false;
output->stop();
return file->close();
}
bool AudioGeneratorAAC::isRunning()
{
return running;
}
bool AudioGeneratorAAC::FillBufferWithValidFrame()
{
buff[0] = 0; // Destroy any existing sync word @ 0
int nextSync;
do {
nextSync = AACFindSyncWord(buff + lastFrameEnd, buffValid - lastFrameEnd);
if (nextSync >= 0) nextSync += lastFrameEnd;
lastFrameEnd = 0;
if (nextSync == -1) {
if (buffValid && buff[buffValid-1]==0xff) { // Could be 1st half of syncword, preserve it...
buff[0] = 0xff;
buffValid = file->read(buff+1, buffLen-1);
if (buffValid==0) return false; // No data available, EOF
} else { // Try a whole new buffer
buffValid = file->read(buff, buffLen-1);
if (buffValid==0) return false; // No data available, EOF
}
}
} while (nextSync == -1);
// Move the frame to start at offset 0 in the buffer
buffValid -= nextSync; // Throw out prior to nextSync
memmove(buff, buff+nextSync, buffValid);
// We have a sync word at 0 now, try and fill remainder of buffer
buffValid += file->read(buff + buffValid, buffLen - buffValid);
return true;
}
bool AudioGeneratorAAC::loop()
{
if (!running) goto done; // Nothing to do here!
// If we've got data, try and pump it out...
while (validSamples) {
lastSample[0] = outSample[curSample*2];
lastSample[1] = outSample[curSample*2 + 1];
if (!output->ConsumeSample(lastSample)) goto done; // Can't send, but no error detected
validSamples--;
curSample++;
}
// No samples available, need to decode a new frame
if (FillBufferWithValidFrame()) {
// buff[0] start of frame, decode it...
unsigned char *inBuff = reinterpret_cast<unsigned char *>(buff);
int bytesLeft = buffValid;
int ret = AACDecode(hAACDecoder, &inBuff, &bytesLeft, outSample);
if (ret) {
// Error, skip the frame...
char buff[48];
sprintf_P(buff, PSTR("AAC decode error %d"), ret);
cb.st(ret, buff);
} else {
lastFrameEnd = buffValid - bytesLeft;
AACFrameInfo fi;
AACGetLastFrameInfo(hAACDecoder, &fi);
if ((int)fi.sampRateOut != (int)lastRate) {
output->SetRate(fi.sampRateOut);
lastRate = fi.sampRateOut;
}
if (fi.nChans != lastChannels) {
output->SetChannels(fi.nChans);
lastChannels = fi.nChans;
}
curSample = 0;
validSamples = fi.outputSamps / lastChannels;
}
} else {
running = false; // No more data, we're done here...
}
done:
file->loop();
output->loop();
return running;
}
bool AudioGeneratorAAC::begin(AudioFileSource *source, AudioOutput *output)
{
if (!source) return false;
file = source;
if (!output) return false;
this->output = output;
if (!file->isOpen()) return false; // Error
output->begin();
// AAC always comes out at 16 bits
output->SetBitsPerSample(16);
memset(buff, 0, buffLen);
memset(outSample, 0, 1024*2*sizeof(int16_t));
running = true;
return true;
}

View File

@ -0,0 +1,64 @@
/*
AudioGeneratorAAC
Audio output generator using the Helix AAC decoder
Copyright (C) 2017 Earle F. Philhower, III
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef _AUDIOGENERATORAAC_H
#define _AUDIOGENERATORAAC_H
#include "AudioGenerator.h"
#include "libhelix-aac/aacdec.h"
class AudioGeneratorAAC : public AudioGenerator
{
public:
AudioGeneratorAAC();
AudioGeneratorAAC(void *preallocateData, int preallocateSize);
virtual ~AudioGeneratorAAC() override;
virtual bool begin(AudioFileSource *source, AudioOutput *output) override;
virtual bool loop() override;
virtual bool stop() override;
virtual bool isRunning() override;
protected:
void *preallocateSpace;
int preallocateSize;
// Helix AAC decoder
HAACDecoder hAACDecoder;
// Input buffering
const int buffLen = 1600;
uint8_t *buff; //[1600]; // File buffer required to store at least a whole compressed frame
int16_t buffValid;
int16_t lastFrameEnd;
bool FillBufferWithValidFrame(); // Read until we get a valid syncword and min(feof, 2048) butes in the buffer
// Output buffering
int16_t *outSample; //[1024 * 2]; // Interleaved L/R
int16_t validSamples;
int16_t curSample;
// Each frame may change this if they're very strange, I guess
unsigned int lastRate;
int lastChannels;
};
#endif

View File

@ -0,0 +1,199 @@
/*
AudioGeneratorFLAC
Audio output generator that plays FLAC audio files
Copyright (C) 2017 Earle F. Philhower, III
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <AudioGeneratorFLAC.h>
AudioGeneratorFLAC::AudioGeneratorFLAC()
{
flac = NULL;
channels = 0;
sampleRate = 0;
bitsPerSample = 0;
buff[0] = NULL;
buff[1] = NULL;
buffPtr = 0;
buffLen = 0;
running = false;
}
AudioGeneratorFLAC::~AudioGeneratorFLAC()
{
if (flac)
FLAC__stream_decoder_delete(flac);
flac = NULL;
}
bool AudioGeneratorFLAC::begin(AudioFileSource *source, AudioOutput *output)
{
if (!source) return false;
file = source;
if (!output) return false;
this->output = output;
if (!file->isOpen()) return false; // Error
flac = FLAC__stream_decoder_new();
if (!flac) return false;
(void)FLAC__stream_decoder_set_md5_checking(flac, false);
FLAC__StreamDecoderInitStatus ret = FLAC__stream_decoder_init_stream(flac, _read_cb, _seek_cb, _tell_cb, _length_cb, _eof_cb, _write_cb, _metadata_cb, _error_cb, reinterpret_cast<void*>(this) );
if (ret != FLAC__STREAM_DECODER_INIT_STATUS_OK) {
FLAC__stream_decoder_delete(flac);
flac = NULL;
return false;
}
output->begin();
running = true;
return true;
}
bool AudioGeneratorFLAC::loop()
{
FLAC__bool ret;
if (!running) goto done;
if (!output->ConsumeSample(lastSample)) goto done; // Try and send last buffered sample
do {
if (buffPtr == buffLen) {
ret = FLAC__stream_decoder_process_single(flac);
if (!ret) {
running = false;
goto done;
} else {
// We might be done...
if (FLAC__stream_decoder_get_state(flac)==FLAC__STREAM_DECODER_END_OF_STREAM) {
running = false;
goto done;
}
unsigned newsr = FLAC__stream_decoder_get_sample_rate(flac);
unsigned newch = FLAC__stream_decoder_get_channels(flac);
unsigned newbps = FLAC__stream_decoder_get_bits_per_sample(flac);
if (newsr != sampleRate) output->SetRate(sampleRate = newsr);
if (newch != channels) output->SetChannels(channels = newch);
if (newbps != bitsPerSample) output->SetBitsPerSample( bitsPerSample = newbps);
}
}
// Check for some weird case where above didn't give any data
if (buffPtr == buffLen) {
goto done; // At some point the flac better error and we'll return
}
if (bitsPerSample <= 16) {
lastSample[AudioOutput::LEFTCHANNEL] = buff[0][buffPtr] & 0xffff;
if (channels==2) lastSample[AudioOutput::RIGHTCHANNEL] = buff[1][buffPtr] & 0xffff;
else lastSample[AudioOutput::RIGHTCHANNEL] = lastSample[AudioOutput::LEFTCHANNEL];
} else if (bitsPerSample <= 24) {
lastSample[AudioOutput::LEFTCHANNEL] = (buff[0][buffPtr]>>8) & 0xffff;
if (channels==2) lastSample[AudioOutput::RIGHTCHANNEL] = (buff[1][buffPtr]>>8) & 0xffff;
else lastSample[AudioOutput::RIGHTCHANNEL] = lastSample[AudioOutput::LEFTCHANNEL];
} else {
lastSample[AudioOutput::LEFTCHANNEL] = (buff[0][buffPtr]>>16) & 0xffff;
if (channels==2) lastSample[AudioOutput::RIGHTCHANNEL] = (buff[1][buffPtr]>>16) & 0xffff;
else lastSample[AudioOutput::RIGHTCHANNEL] = lastSample[AudioOutput::LEFTCHANNEL];
}
buffPtr++;
} while (running && output->ConsumeSample(lastSample));
done:
file->loop();
output->loop();
return running;
}
bool AudioGeneratorFLAC::stop()
{
if (flac)
FLAC__stream_decoder_delete(flac);
flac = NULL;
running = false;
output->stop();
return true;
}
bool AudioGeneratorFLAC::isRunning()
{
return running;
}
FLAC__StreamDecoderReadStatus AudioGeneratorFLAC::read_cb(const FLAC__StreamDecoder *decoder, FLAC__byte buffer[], size_t *bytes)
{
(void) decoder;
if (*bytes==0) return FLAC__STREAM_DECODER_READ_STATUS_ABORT;
*bytes = file->read(buffer, sizeof(FLAC__byte) * (*bytes));
if (*bytes==0) return FLAC__STREAM_DECODER_READ_STATUS_END_OF_STREAM;
return FLAC__STREAM_DECODER_READ_STATUS_CONTINUE;
}
FLAC__StreamDecoderSeekStatus AudioGeneratorFLAC::seek_cb(const FLAC__StreamDecoder *decoder, FLAC__uint64 absolute_byte_offset)
{
(void) decoder;
if (!file->seek((int32_t)absolute_byte_offset, 0)) return FLAC__STREAM_DECODER_SEEK_STATUS_ERROR;
return FLAC__STREAM_DECODER_SEEK_STATUS_OK;
}
FLAC__StreamDecoderTellStatus AudioGeneratorFLAC::tell_cb(const FLAC__StreamDecoder *decoder, FLAC__uint64 *absolute_byte_offset)
{
(void) decoder;
*absolute_byte_offset = file->getPos();
return FLAC__STREAM_DECODER_TELL_STATUS_OK;
}
FLAC__StreamDecoderLengthStatus AudioGeneratorFLAC::length_cb(const FLAC__StreamDecoder *decoder, FLAC__uint64 *stream_length)
{
(void) decoder;
*stream_length = file->getSize();
return FLAC__STREAM_DECODER_LENGTH_STATUS_OK;
}
FLAC__bool AudioGeneratorFLAC::eof_cb(const FLAC__StreamDecoder *decoder)
{
(void) decoder;
if (file->getPos() >= file->getSize()) return true;
return false;
}
FLAC__StreamDecoderWriteStatus AudioGeneratorFLAC::write_cb(const FLAC__StreamDecoder *decoder, const FLAC__Frame *frame, const FLAC__int32 *const buffer[])
{
(void) decoder;
// Hackish warning here. FLAC sends the buffer but doesn't free it until the next call to decode_frame, so we stash
// the pointers here and use it in our loop() instead of memcpy()'ing into yet another buffer.
buffLen = frame->header.blocksize;
buff[0] = buffer[0];
if (frame->header.channels>1) buff[1] = buffer[1];
else buff[1] = buffer[0];
buffPtr = 0;
return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE;
}
void AudioGeneratorFLAC::metadata_cb(const FLAC__StreamDecoder *decoder, const FLAC__StreamMetadata *metadata)
{
(void) decoder;
(void) metadata;
audioLogger->printf_P(PSTR("Metadata\n"));
}
char AudioGeneratorFLAC::error_cb_str[64];
void AudioGeneratorFLAC::error_cb(const FLAC__StreamDecoder *decoder, FLAC__StreamDecoderErrorStatus status)
{
(void) decoder;
strncpy_P(error_cb_str, FLAC__StreamDecoderErrorStatusString[status], 64);
cb.st((int)status, error_cb_str);
}

View File

@ -0,0 +1,89 @@
/*
AudioGeneratorFLAC
Audio output generator that plays FLAC audio files
Copyright (C) 2017 Earle F. Philhower, III
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef _AUDIOGENERATORFLAC_H
#define _AUDIOGENERATORFLAC_H
#include <AudioGenerator.h>
extern "C" {
#include "libflac/FLAC/stream_decoder.h"
};
class AudioGeneratorFLAC : public AudioGenerator
{
public:
AudioGeneratorFLAC();
virtual ~AudioGeneratorFLAC() override;
virtual bool begin(AudioFileSource *source, AudioOutput *output) override;
virtual bool loop() override;
virtual bool stop() override;
virtual bool isRunning() override;
protected:
// FLAC info
uint16_t channels;
uint32_t sampleRate;
uint16_t bitsPerSample;
// We need to buffer some data in-RAM to avoid doing 1000s of small reads
const int *buff[2];
uint16_t buffPtr;
uint16_t buffLen;
FLAC__StreamDecoder *flac;
// FLAC callbacks, need static functions to bounce into c++ from c
static FLAC__StreamDecoderReadStatus _read_cb(const FLAC__StreamDecoder *decoder, FLAC__byte buffer[], size_t *bytes, void *client_data) {
return static_cast<AudioGeneratorFLAC*>(client_data)->read_cb(decoder, buffer, bytes);
};
static FLAC__StreamDecoderSeekStatus _seek_cb(const FLAC__StreamDecoder *decoder, FLAC__uint64 absolute_byte_offset, void *client_data) {
return static_cast<AudioGeneratorFLAC*>(client_data)->seek_cb(decoder, absolute_byte_offset);
};
static FLAC__StreamDecoderTellStatus _tell_cb(const FLAC__StreamDecoder *decoder, FLAC__uint64 *absolute_byte_offset, void *client_data) {
return static_cast<AudioGeneratorFLAC*>(client_data)->tell_cb(decoder, absolute_byte_offset);
};
static FLAC__StreamDecoderLengthStatus _length_cb(const FLAC__StreamDecoder *decoder, FLAC__uint64 *stream_length, void *client_data) {
return static_cast<AudioGeneratorFLAC*>(client_data)->length_cb(decoder, stream_length);
};
static FLAC__bool _eof_cb(const FLAC__StreamDecoder *decoder, void *client_data) {
return static_cast<AudioGeneratorFLAC*>(client_data)->eof_cb(decoder);
};
static FLAC__StreamDecoderWriteStatus _write_cb(const FLAC__StreamDecoder *decoder, const FLAC__Frame *frame, const FLAC__int32 *const buffer[], void *client_data) {
return static_cast<AudioGeneratorFLAC*>(client_data)->write_cb(decoder, frame, buffer);
};
static void _metadata_cb(const FLAC__StreamDecoder *decoder, const FLAC__StreamMetadata *metadata, void *client_data) {
static_cast<AudioGeneratorFLAC*>(client_data)->metadata_cb(decoder, metadata);
};
static void _error_cb(const FLAC__StreamDecoder *decoder, FLAC__StreamDecoderErrorStatus status, void *client_data) {
static_cast<AudioGeneratorFLAC*>(client_data)->error_cb(decoder, status);
};
// Actual FLAC callbacks
FLAC__StreamDecoderReadStatus read_cb(const FLAC__StreamDecoder *decoder, FLAC__byte buffer[], size_t *bytes);
FLAC__StreamDecoderSeekStatus seek_cb(const FLAC__StreamDecoder *decoder, FLAC__uint64 absolute_byte_offset);
FLAC__StreamDecoderTellStatus tell_cb(const FLAC__StreamDecoder *decoder, FLAC__uint64 *absolute_byte_offset);
FLAC__StreamDecoderLengthStatus length_cb(const FLAC__StreamDecoder *decoder, FLAC__uint64 *stream_length);
FLAC__bool eof_cb(const FLAC__StreamDecoder *decoder);
FLAC__StreamDecoderWriteStatus write_cb(const FLAC__StreamDecoder *decoder, const FLAC__Frame *frame, const FLAC__int32 *const buffer[]);
void metadata_cb(const FLAC__StreamDecoder *decoder, const FLAC__StreamMetadata *metadata);
static char error_cb_str[64];
void error_cb(const FLAC__StreamDecoder *decoder, FLAC__StreamDecoderErrorStatus status);
};
#endif

View File

@ -0,0 +1,639 @@
/*
AudioGeneratorMIDI
Audio output generator that plays MIDI files using a SF2 SoundFont
Copyright (C) 2017 Earle F. Philhower, III
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/*
The MIDI processing engine is a heavily modified version of MIDITONES,
by Len Shustek, https://github.com/LenShustek/miditones .
Whereas MIDITONES original simply parsed a file beforehand to a byte
stream to be played by another program, this does the parsing and
playback in real-time.
Here's his original header/readme w/MIT license, which is subsumed by the
GPL license of the ESP8266Audio project.
*/
/***************************************************************************
MIDITONES: Convert a MIDI file into a simple bytestream of notes
-------------------------------------------------------------------------
The MIT License (MIT)
Copyright (c) 2011,2013,2015,2016, Len Shustek
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF, OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
**************************************************************************/
#include "AudioGeneratorMIDI.h"
#pragma GCC optimize ("O3")
#define TSF_NO_STDIO
#define TSF_IMPLEMENTATION
#include "libtinysoundfont/tsf.h"
/**************** utility routines **********************/
/* announce a fatal MIDI file format error */
void AudioGeneratorMIDI::midi_error(const char *msg, int curpos)
{
cb.st(curpos, msg);
#if 0
int ptr;
audioLogger->printf("---> MIDI file error at position %04X (%d): %s\n", (uint16_t) curpos, (uint16_t) curpos, msg);
/* print some bytes surrounding the error */
ptr = curpos - 16;
if (ptr < 0) ptr = 0;
buffer.seek( buffer.data, ptr );
for (int i = 0; i < 32; i++) {
char c;
buffer.read (buffer.data, &c, 1);
audioLogger->printf((ptr + i) == curpos ? " [%02X] " : "%02X ", (int) c & 0xff);
}
audioLogger->printf("\n");
#endif
running = false;
}
/* check that we have a specified number of bytes left in the buffer */
void AudioGeneratorMIDI::chk_bufdata (int ptr, unsigned long int len) {
if ((unsigned) (ptr + len) > buflen)
midi_error ("data missing", ptr);
}
/* fetch big-endian numbers */
uint16_t AudioGeneratorMIDI::rev_short (uint16_t val) {
return ((val & 0xff) << 8) | ((val >> 8) & 0xff);
}
uint32_t AudioGeneratorMIDI::rev_long (uint32_t val) {
return (((rev_short ((uint16_t) val) & 0xffff) << 16) |
(rev_short ((uint16_t) (val >> 16)) & 0xffff));
}
/************** process the MIDI file header *****************/
void AudioGeneratorMIDI::process_header (void) {
struct midi_header hdr;
unsigned int time_division;
chk_bufdata (hdrptr, sizeof (struct midi_header));
buffer.seek (buffer.data, hdrptr);
buffer.read (buffer.data, &hdr, sizeof (hdr));
if (!charcmp ((char *) hdr.MThd, "MThd"))
midi_error ("Missing 'MThd'", hdrptr);
num_tracks = rev_short (hdr.number_of_tracks);
time_division = rev_short (hdr.time_division);
if (time_division < 0x8000)
ticks_per_beat = time_division;
else
ticks_per_beat = ((time_division >> 8) & 0x7f) /* SMTE frames/sec */ *(time_division & 0xff); /* ticks/SMTE frame */
hdrptr += rev_long (hdr.header_size) + 8; /* point past header to track header, presumably. */
return;
}
/**************** Process a MIDI track header *******************/
void AudioGeneratorMIDI::start_track (int tracknum) {
struct track_header hdr;
unsigned long tracklen;
chk_bufdata (hdrptr, sizeof (struct track_header));
buffer.seek (buffer.data, hdrptr);
buffer.read (buffer.data, &hdr, sizeof (hdr));
if (!charcmp ((char *) (hdr.MTrk), "MTrk"))
midi_error ("Missing 'MTrk'", hdrptr);
tracklen = rev_long (hdr.track_size);
hdrptr += sizeof (struct track_header); /* point past header */
chk_bufdata (hdrptr, tracklen);
track[tracknum].trkptr = hdrptr;
hdrptr += tracklen; /* point to the start of the next track */
track[tracknum].trkend = hdrptr; /* the point past the end of the track */
}
unsigned char AudioGeneratorMIDI::buffer_byte (int offset) {
unsigned char c;
buffer.seek (buffer.data, offset);
buffer.read (buffer.data, &c, 1);
return c;
}
unsigned short AudioGeneratorMIDI::buffer_short (int offset) {
unsigned short s;
buffer.seek (buffer.data, offset);
buffer.read (buffer.data, &s, sizeof (short));
return s;
}
unsigned int AudioGeneratorMIDI::buffer_int32 (int offset) {
uint32_t i;
buffer.seek (buffer.data, offset);
buffer.read (buffer.data, &i, sizeof (i));
return i;
}
/* Get a MIDI-style variable-length integer */
unsigned long AudioGeneratorMIDI::get_varlen (int *ptr) {
/* Get a 1-4 byte variable-length value and adjust the pointer past it.
These are a succession of 7-bit values with a MSB bit of zero marking the end */
unsigned long val;
int i, byte;
val = 0;
for (i = 0; i < 4; ++i) {
byte = buffer_byte ((*ptr)++);
val = (val << 7) | (byte & 0x7f);
if (!(byte & 0x80))
return val;
}
return val;
}
/*************** Process the MIDI track data ***************************/
/* Skip in the track for the next "note on", "note off" or "set tempo" command,
then record that information in the track status block and return. */
void AudioGeneratorMIDI::find_note (int tracknum) {
unsigned long int delta_time;
int event, chan;
int note, velocity, controller, pressure, pitchbend, instrument;
int meta_cmd, meta_length;
unsigned long int sysex_length;
struct track_status *t;
const char *tag;
/* process events */
t = &track[tracknum]; /* our track status structure */
while (t->trkptr < t->trkend) {
delta_time = get_varlen (&t->trkptr);
t->time += delta_time;
if (buffer_byte (t->trkptr) < 0x80)
event = t->last_event; /* using "running status": same event as before */
else { /* otherwise get new "status" (event type) */
event = buffer_byte (t->trkptr++);
}
if (event == 0xff) { /* meta-event */
meta_cmd = buffer_byte (t->trkptr++);
meta_length = get_varlen(&t->trkptr);
switch (meta_cmd) {
case 0x00:
break;
case 0x01:
tag = "description";
goto show_text;
case 0x02:
tag = "copyright";
goto show_text;
case 0x03:
tag = "track name";
goto show_text;
case 0x04:
tag = "instrument name";
goto show_text;
case 0x05:
tag = "lyric";
goto show_text;
case 0x06:
tag = "marked point";
goto show_text;
case 0x07:
tag = "cue point";
show_text:
break;
case 0x20:
break;
case 0x2f:
break;
case 0x51: /* tempo: 3 byte big-endian integer! */
t->cmd = CMD_TEMPO;
t->tempo = rev_long (buffer_int32 (t->trkptr - 1)) & 0xffffffL;
t->trkptr += meta_length;
return;
case 0x54:
break;
case 0x58:
break;
case 0x59:
break;
case 0x7f:
tag = "sequencer data";
goto show_hex;
default: /* unknown meta command */
tag = "???";
show_hex:
break;
}
t->trkptr += meta_length;
}
else if (event < 0x80)
midi_error ("Unknown MIDI event type", t->trkptr);
else {
if (event < 0xf0)
t->last_event = event; // remember "running status" if not meta or sysex event
chan = event & 0xf;
t->chan = chan;
switch (event >> 4) {
case 0x8:
t->note = buffer_byte (t->trkptr++);
velocity = buffer_byte (t->trkptr++);
note_off:
t->cmd = CMD_STOPNOTE;
return; /* stop processing and return */
case 0x9:
t->note = buffer_byte (t->trkptr++);
velocity = buffer_byte (t->trkptr++);
if (velocity == 0) /* some scores use note-on with zero velocity for off! */
goto note_off;
t->velocity = velocity;
t->cmd = CMD_PLAYNOTE;
return; /* stop processing and return */
case 0xa:
note = buffer_byte (t->trkptr++);
velocity = buffer_byte (t->trkptr++);
break;
case 0xb:
controller = buffer_byte (t->trkptr++);
velocity = buffer_byte (t->trkptr++);
break;
case 0xc:
instrument = buffer_byte (t->trkptr++);
midi_chan_instrument[chan] = instrument; // record new instrument for this channel
break;
case 0xd:
pressure = buffer_byte (t->trkptr++);
break;
case 0xe:
pitchbend = buffer_byte (t->trkptr) | (buffer_byte (t->trkptr + 1) << 7);
t->trkptr += 2;
break;
case 0xf:
sysex_length = get_varlen (&t->trkptr);
t->trkptr += sysex_length;
break;
default:
midi_error ("Unknown MIDI command", t->trkptr);
}
}
}
t->cmd = CMD_TRACKDONE; /* no more notes to process */
++tracks_done;
// Remove unused warnings..maybe some day we'll look at these
(void)note;
(void)controller;
(void)pressure;
(void)pitchbend;
(void)tag;
}
// Open file, parse headers, get ready tio process MIDI
void AudioGeneratorMIDI::PrepareMIDI(AudioFileSource *src)
{
MakeStreamFromAFS(src, &afsMIDI);
tsf_stream_wrap_cached(&afsMIDI, 32, 64, &buffer);
buflen = buffer.size (buffer.data);
/* process the MIDI file header */
hdrptr = buffer.tell (buffer.data); /* pointer to file and track headers */
process_header ();
printf (" Processing %d tracks.\n", num_tracks);
if (num_tracks > MAX_TRACKS)
midi_error ("Too many tracks", buffer.tell (buffer.data));
/* initialize processing of all the tracks */
for (tracknum = 0; tracknum < num_tracks; ++tracknum) {
start_track (tracknum); /* process the track header */
find_note (tracknum); /* position to the first note on/off */
}
notes_skipped = 0;
tracknum = 0;
earliest_tracknum = 0;
earliest_time = 0;
}
// Parses the note on/offs until we are ready to render some more samples. Then return the
// total number of samples to render before we need to be called again
int AudioGeneratorMIDI::PlayMIDI()
{
/* Continue processing all tracks, in an order based on the simulated time.
This is not unlike multiway merging used for tape sorting algoritms in the 50's! */
do { /* while there are still track notes to process */
static struct track_status *trk;
static struct tonegen_status *tg;
static int tgnum;
static int count_tracks;
static unsigned long delta_time, delta_msec;
/* Find the track with the earliest event time,
and output a delay command if time has advanced.
A potential improvement: If there are multiple tracks with the same time,
first do the ones with STOPNOTE as the next command, if any. That would
help avoid running out of tone generators. In practice, though, most MIDI
files do all the STOPNOTEs first anyway, so it won't have much effect.
*/
earliest_time = 0x7fffffff;
/* Usually we start with the track after the one we did last time (tracknum),
so that if we run out of tone generators, we have been fair to all the tracks.
The alternate "strategy1" says we always start with track 0, which means
that we favor early tracks over later ones when there aren't enough tone generators.
*/
count_tracks = num_tracks;
do {
if (++tracknum >= num_tracks)
tracknum = 0;
trk = &track[tracknum];
if (trk->cmd != CMD_TRACKDONE && trk->time < earliest_time) {
earliest_time = trk->time;
earliest_tracknum = tracknum;
}
} while (--count_tracks);
tracknum = earliest_tracknum; /* the track we picked */
trk = &track[tracknum];
if (earliest_time < timenow)
midi_error ("INTERNAL: time went backwards", trk->trkptr);
/* If time has advanced, output a "delay" command */
delta_time = earliest_time - timenow;
if (delta_time) {
/* Convert ticks to milliseconds based on the current tempo */
unsigned long long temp;
temp = ((unsigned long long) delta_time * tempo) / ticks_per_beat;
delta_msec = temp / 1000; // get around LCC compiler bug
if (delta_msec > 0x7fff)
midi_error ("INTERNAL: time delta too big", trk->trkptr);
int samples = (((int) delta_msec) * freq) / 1000;
timenow = earliest_time;
return samples;
}
timenow = earliest_time;
/* If this track event is "set tempo", just change the global tempo.
That affects how we generate "delay" commands. */
if (trk->cmd == CMD_TEMPO) {
tempo = trk->tempo;
find_note (tracknum);
}
/* If this track event is "stop note", process it and all subsequent "stop notes" for this track
that are happening at the same time. Doing so frees up as many tone generators as possible. */
else if (trk->cmd == CMD_STOPNOTE)
do {
// stop a note
for (tgnum = 0; tgnum < num_tonegens; ++tgnum) { /* find which generator is playing it */
tg = &tonegen[tgnum];
if (tg->playing && tg->track == tracknum && tg->note == trk->note) {
tsf_note_off (g_tsf, tg->instrument, tg->note);
tg->playing = false;
trk->tonegens[tgnum] = false;
}
}
find_note (tracknum); // use up the note
} while (trk->cmd == CMD_STOPNOTE && trk->time == timenow);
/* If this track event is "start note", process only it.
Don't do more than one, so we allow other tracks their chance at grabbing tone generators. */
else if (trk->cmd == CMD_PLAYNOTE) {
bool foundgen = false;
/* if not, then try for any free tone generator */
if (!foundgen)
for (tgnum = 0; tgnum < num_tonegens; ++tgnum) {
tg = &tonegen[tgnum];
if (!tg->playing) {
foundgen = true;
break;
}
}
if (foundgen) {
if (tgnum + 1 > num_tonegens_used)
num_tonegens_used = tgnum + 1;
tg->playing = true;
tg->track = tracknum;
tg->note = trk->note;
trk->tonegens[tgnum] = true;
trk->preferred_tonegen = tgnum;
if (tg->instrument != midi_chan_instrument[trk->chan]) { /* new instrument for this generator */
tg->instrument = midi_chan_instrument[trk->chan];
}
tsf_note_on (g_tsf, tg->instrument, tg->note, trk->velocity / 127.0); // velocity = 0...127
} else {
++notes_skipped;
}
find_note (tracknum); // use up the note
}
}
while (tracks_done < num_tracks);
return -1; // EOF
}
void AudioGeneratorMIDI::StopMIDI()
{
buffer.close(buffer.data);
tsf_close(g_tsf);
printf (" %s %d tone generators were used.\n",
num_tonegens_used < num_tonegens ? "Only" : "All", num_tonegens_used);
if (notes_skipped)
printf
(" %d notes were skipped because there weren't enough tone generators.\n", notes_skipped);
printf (" Done.\n");
}
bool AudioGeneratorMIDI::begin(AudioFileSource *src, AudioOutput *out)
{
// Clear out status variables
for (int i=0; i<MAX_TONEGENS; i++) memset(&tonegen[i], 0, sizeof(struct tonegen_status));
for (int i=0; i<MAX_TRACKS; i++) memset(&track[i], 0, sizeof(struct track_status));
memset(midi_chan_instrument, 0, sizeof(midi_chan_instrument));
g_tsf = tsf_load(&afsSF2);
if (!g_tsf) return false;
tsf_set_output (g_tsf, TSF_MONO, freq, -10 /* dB gain -10 */ );
if (!out->SetRate( freq )) return false;
if (!out->SetBitsPerSample( 16 )) return false;
if (!out->SetChannels( 1 )) return false;
if (!out->begin()) return false;
output = out;
file = src;
running = true;
PrepareMIDI(src);
samplesToPlay = 0;
numSamplesRendered = 0;
sentSamplesRendered = 0;
sawEOF = false;
return running;
}
bool AudioGeneratorMIDI::loop()
{
static int c = 0;
if (!running) goto done; // Nothing to do here!
// First, try and push in the stored sample. If we can't, then punt and try later
if (!output->ConsumeSample(lastSample)) goto done; // Can't send, but no error detected
// Try and stuff the buffer one sample at a time
do {
c++;
if (c%44100 == 0) yield();
play:
if (sentSamplesRendered < numSamplesRendered) {
lastSample[AudioOutput::LEFTCHANNEL] = samplesRendered[sentSamplesRendered];
lastSample[AudioOutput::RIGHTCHANNEL] = samplesRendered[sentSamplesRendered];
sentSamplesRendered++;
} else if (samplesToPlay) {
numSamplesRendered = sizeof(samplesRendered)/sizeof(samplesRendered[0]);
if ((int)samplesToPlay < (int)(sizeof(samplesRendered)/sizeof(samplesRendered[0]))) numSamplesRendered = samplesToPlay;
tsf_render_short_fast(g_tsf, samplesRendered, numSamplesRendered, 0);
lastSample[AudioOutput::LEFTCHANNEL] = samplesRendered[0];
lastSample[AudioOutput::RIGHTCHANNEL] = samplesRendered[0];
sentSamplesRendered = 1;
samplesToPlay -= numSamplesRendered;
} else {
numSamplesRendered = 0;
sentSamplesRendered = 0;
if (sawEOF) {
running = false;
} else {
samplesToPlay = PlayMIDI();
if (samplesToPlay == -1) {
sawEOF = true;
samplesToPlay = freq / 2;
}
goto play;
}
}
} while (running && output->ConsumeSample(lastSample));
done:
file->loop();
output->loop();
return running;
}
bool AudioGeneratorMIDI::stop()
{
StopMIDI();
output->stop();
return true;
}
int AudioGeneratorMIDI::afs_read(void *data, void *ptr, unsigned int size)
{
AudioFileSource *s = reinterpret_cast<AudioFileSource *>(data);
return s->read(ptr, size);
}
int AudioGeneratorMIDI::afs_tell(void *data)
{
AudioFileSource *s = reinterpret_cast<AudioFileSource *>(data);
return s->getPos();
}
int AudioGeneratorMIDI::afs_skip(void *data, unsigned int count)
{
AudioFileSource *s = reinterpret_cast<AudioFileSource *>(data);
return s->seek(count, SEEK_CUR);
}
int AudioGeneratorMIDI::afs_seek(void *data, unsigned int pos)
{
AudioFileSource *s = reinterpret_cast<AudioFileSource *>(data);
return s->seek(pos, SEEK_SET);
}
int AudioGeneratorMIDI::afs_close(void *data)
{
AudioFileSource *s = reinterpret_cast<AudioFileSource *>(data);
return s->close();
}
int AudioGeneratorMIDI::afs_size(void *data)
{
AudioFileSource *s = reinterpret_cast<AudioFileSource *>(data);
return s->getSize();
}
void AudioGeneratorMIDI::MakeStreamFromAFS(AudioFileSource *src, tsf_stream *afs)
{
afs->data = reinterpret_cast<void*>(src);
afs->read = &afs_read;
afs->tell = &afs_tell;
afs->skip = &afs_skip;
afs->seek = &afs_seek;
afs->close = &afs_close;
afs->size = &afs_size;
}

View File

@ -0,0 +1,181 @@
/*
AudioGeneratorMIDI
Audio output generator that plays MIDI files using a SF2 SoundFont
Copyright (C) 2017 Earle F. Philhower, III
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef _AUDIOGENERATORMIDI_H
#define _AUDIOGENERATORMIDI_H
#include "AudioGenerator.h"
#define TSF_NO_STDIO
#include "libtinysoundfont/tsf.h"
class AudioGeneratorMIDI : public AudioGenerator
{
public:
AudioGeneratorMIDI() { freq=44100; running = false; };
virtual ~AudioGeneratorMIDI() override {};
bool SetSoundfont(AudioFileSource *newsf2) {
if (isRunning()) return false;
sf2 = newsf2;
MakeStreamFromAFS(sf2, &afsSF2);
return true;
}
bool SetSampleRate(int newfreq) {
if (isRunning()) return false;
freq = newfreq;
return true;
}
virtual bool begin(AudioFileSource *mid, AudioOutput *output) override;
virtual bool loop() override;
virtual bool stop() override;
virtual bool isRunning() override { return running; };
private:
int freq;
tsf *g_tsf;
struct tsf_stream buffer;
struct tsf_stream afsMIDI;
struct tsf_stream afsSF2;
AudioFileSource *sf2;
AudioFileSource *midi;
protected:
struct midi_header {
int8_t MThd[4];
uint32_t header_size;
uint16_t format_type;
uint16_t number_of_tracks;
uint16_t time_division;
};
struct track_header {
int8_t MTrk[4];
uint32_t track_size;
};
enum { MAX_TONEGENS = 32, /* max tone generators: tones we can play simultaneously */
MAX_TRACKS = 24
}; /* max number of MIDI tracks we will process */
int hdrptr;
unsigned long buflen;
int num_tracks;
int tracks_done = 0;
int num_tonegens = MAX_TONEGENS;
int num_tonegens_used = 0;
unsigned int ticks_per_beat = 240;
unsigned long timenow = 0;
unsigned long tempo; /* current tempo in usec/qnote */
// State needed for PlayMID()
int notes_skipped = 0;
int tracknum = 0;
int earliest_tracknum = 0;
unsigned long earliest_time = 0;
struct tonegen_status { /* current status of a tone generator */
bool playing; /* is it playing? */
char track; /* if so, which track is the note from? */
char note; /* what note is playing? */
char instrument; /* what instrument? */
} tonegen[MAX_TONEGENS];
struct track_status { /* current processing point of a MIDI track */
int trkptr; /* ptr to the next note change */
int trkend; /* ptr past the end of the track */
unsigned long time; /* what time we're at in the score */
unsigned long tempo; /* the tempo last set, in usec per qnote */
unsigned int preferred_tonegen; /* for strategy2, try to use this generator */
unsigned char cmd; /* CMD_xxxx next to do */
unsigned char note; /* for which note */
unsigned char chan; /* from which channel it was */
unsigned char velocity; /* the current volume */
unsigned char last_event; /* the last event, for MIDI's "running status" */
bool tonegens[MAX_TONEGENS]; /* which tone generators our notes are playing on */
} track[MAX_TRACKS];
int midi_chan_instrument[16]; /* which instrument is currently being played on each channel */
/* output bytestream commands, which are also stored in track_status.cmd */
enum { CMD_PLAYNOTE = 0x90, /* play a note: low nibble is generator #, note is next byte */
CMD_STOPNOTE = 0x80, /* stop a note: low nibble is generator # */
CMD_INSTRUMENT = 0xc0, /* change instrument; low nibble is generator #, instrument is next byte */
CMD_RESTART = 0xe0, /* restart the score from the beginning */
CMD_STOP = 0xf0, /* stop playing */
CMD_TEMPO = 0xFE, /* tempo in usec per quarter note ("beat") */
CMD_TRACKDONE = 0xFF
}; /* no more data left in this track */
/* portable string length */
int strlength (const char *str) {
int i;
for (i = 0; str[i] != '\0'; ++i);
return i;
}
/* match a constant character sequence */
int charcmp (const char *buf, const char *match) {
int len, i;
len = strlength (match);
for (i = 0; i < len; ++i)
if (buf[i] != match[i])
return 0;
return 1;
}
unsigned char buffer_byte (int offset);
unsigned short buffer_short (int offset);
unsigned int buffer_int32 (int offset);
void midi_error (const char *msg, int curpos);
void chk_bufdata (int ptr, unsigned long int len);
uint16_t rev_short (uint16_t val);
uint32_t rev_long (uint32_t val);
void process_header (void);
void start_track (int tracknum);
unsigned long get_varlen (int *ptr);
void find_note (int tracknum);
void PrepareMIDI(AudioFileSource *src);
int PlayMIDI();
void StopMIDI();
// tsf_stream <-> AudioFileSource
static int afs_read(void *data, void *ptr, unsigned int size);
static int afs_tell(void *data);
static int afs_skip(void *data, unsigned int count);
static int afs_seek(void *data, unsigned int pos);
static int afs_close(void *data);
static int afs_size(void *data);
void MakeStreamFromAFS(AudioFileSource *src, tsf_stream *afs);
int samplesToPlay;
bool sawEOF;
int numSamplesRendered;
int sentSamplesRendered ;
short samplesRendered[256];
};
#endif

View File

@ -0,0 +1,876 @@
/*
AudioGeneratorMOD
Audio output generator that plays Amiga MOD tracker files
Copyright (C) 2017 Earle F. Philhower, III
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#define PGM_READ_UNALIGNED 0
#include "AudioGeneratorMOD.h"
/*
Ported/hacked out from STELLARPLAYER by Ronen K.
http://mobile4dev.blogspot.com/2012/11/stellaris-launchpad-mod-player.html
A version exists in GitHub at https://github.com/steveway/stellarplayer
and also at https://github.com/MikesModz/StellarPlayer
Both which were themselves a port of the PIC32 MOD player
https://www.youtube.com/watch?v=i3Yl0TISQBE (seems to no longer be available.)
Most changes involved reducing memory usage by changing data structures,
moving constants to PROGMEM and minor tweaks to allow non pow2 buffer sizes.
*/
#pragma GCC optimize ("O3")
#define NOTE(r, c) (Player.currentPattern.note8[r][c]==NONOTE8?NONOTE:8*Player.currentPattern.note8[r][c])
#ifndef min
#define min(X,Y) ((X) < (Y) ? (X) : (Y))
#endif
AudioGeneratorMOD::AudioGeneratorMOD()
{
sampleRate = 44100;
fatBufferSize = 6 * 1024;
stereoSeparation = 32;
mixerTick = 0;
usePAL = false;
UpdateAmiga();
running = false;
file = NULL;
output = NULL;
}
AudioGeneratorMOD::~AudioGeneratorMOD()
{
// Free any remaining buffers
for (int i = 0; i < CHANNELS; i++) {
FatBuffer.channels[i] = NULL;
}
}
bool AudioGeneratorMOD::stop()
{
// We may be stopping because of allocation failures, so always deallocate
for (int i = 0; i < CHANNELS; i++) {
free(FatBuffer.channels[i]);
FatBuffer.channels[i] = NULL;
}
if (file) file->close();
running = false;
output->stop();
return true;
}
bool AudioGeneratorMOD::loop()
{
if (!running) goto done; // Easy-peasy
// First, try and push in the stored sample. If we can't, then punt and try later
if (!output->ConsumeSample(lastSample)) goto done; // FIFO full, wait...
// Now advance enough times to fill the i2s buffer
do {
if (mixerTick == 0) {
running = RunPlayer();
if (!running) {
stop();
goto done;
}
mixerTick = Player.samplesPerTick;
}
GetSample( lastSample );
mixerTick--;
} while (output->ConsumeSample(lastSample));
done:
file->loop();
output->loop();
// We'll be left with one sample still in our buffer because it couldn't fit in the FIFO
return running;
}
bool AudioGeneratorMOD::begin(AudioFileSource *source, AudioOutput *out)
{
if (running) stop();
if (!source) return false;
file = source;
if (!out) return false;
output = out;
if (!file->isOpen()) return false; // Can't read the file!
// Set the output values properly
if (!output->SetRate(sampleRate)) return false;
if (!output->SetBitsPerSample(16)) return false;
if (!output->SetChannels(2)) return false;
if (!output->begin()) return false;
UpdateAmiga();
for (int i = 0; i < CHANNELS; i++) {
FatBuffer.channels[i] = reinterpret_cast<uint8_t*>(malloc(fatBufferSize));
if (!FatBuffer.channels[i]) {
stop();
return false;
}
}
if (!LoadMOD()) {
stop();
return false;
}
running = true;
return true;
}
// Sorted Amiga periods
static const uint16_t amigaPeriods[296] PROGMEM = {
907, 900, 894, 887, 881, 875, 868, 862, // -8 to -1
856, 850, 844, 838, 832, 826, 820, 814, // C-1 to +7
808, 802, 796, 791, 785, 779, 774, 768, // C#1 to +7
762, 757, 752, 746, 741, 736, 730, 725, // D-1 to +7
720, 715, 709, 704, 699, 694, 689, 684, // D#1 to +7
678, 675, 670, 665, 660, 655, 651, 646, // E-1 to +7
640, 636, 632, 628, 623, 619, 614, 610, // F-1 to +7
604, 601, 597, 592, 588, 584, 580, 575, // F#1 to +7
570, 567, 563, 559, 555, 551, 547, 543, // G-1 to +7
538, 535, 532, 528, 524, 520, 516, 513, // G#1 to +7
508, 505, 502, 498, 494, 491, 487, 484, // A-1 to +7
480, 477, 474, 470, 467, 463, 460, 457, // A#1 to +7
453, 450, 447, 444, 441, 437, 434, 431, // B-1 to +7
428, 425, 422, 419, 416, 413, 410, 407, // C-2 to +7
404, 401, 398, 395, 392, 390, 387, 384, // C#2 to +7
381, 379, 376, 373, 370, 368, 365, 363, // D-2 to +7
360, 357, 355, 352, 350, 347, 345, 342, // D#2 to +7
339, 337, 335, 332, 330, 328, 325, 323, // E-2 to +7
320, 318, 316, 314, 312, 309, 307, 305, // F-2 to +7
302, 300, 298, 296, 294, 292, 290, 288, // F#2 to +7
285, 284, 282, 280, 278, 276, 274, 272, // G-2 to +7
269, 268, 266, 264, 262, 260, 258, 256, // G#2 to +7
254, 253, 251, 249, 247, 245, 244, 242, // A-2 to +7
240, 238, 237, 235, 233, 232, 230, 228, // A#2 to +7
226, 225, 223, 222, 220, 219, 217, 216, // B-2 to +7
214, 212, 211, 209, 208, 206, 205, 203, // C-3 to +7
202, 200, 199, 198, 196, 195, 193, 192, // C#3 to +7
190, 189, 188, 187, 185, 184, 183, 181, // D-3 to +7
180, 179, 177, 176, 175, 174, 172, 171, // D#3 to +7
170, 169, 167, 166, 165, 164, 163, 161, // E-3 to +7
160, 159, 158, 157, 156, 155, 154, 152, // F-3 to +7
151, 150, 149, 148, 147, 146, 145, 144, // F#3 to +7
143, 142, 141, 140, 139, 138, 137, 136, // G-3 to +7
135, 134, 133, 132, 131, 130, 129, 128, // G#3 to +7
127, 126, 125, 125, 123, 123, 122, 121, // A-3 to +7
120, 119, 118, 118, 117, 116, 115, 114, // A#3 to +7
113, 113, 112, 111, 110, 109, 109, 108 // B-3 to +7
};
#define ReadAmigaPeriods(a) (uint16_t)pgm_read_word(amigaPeriods + (a))
static const uint8_t sine[64] PROGMEM = {
0, 24, 49, 74, 97, 120, 141, 161,
180, 197, 212, 224, 235, 244, 250, 253,
255, 253, 250, 244, 235, 224, 212, 197,
180, 161, 141, 120, 97, 74, 49, 24
};
#define ReadSine(a) pgm_read_byte(sine + (a))
static inline uint16_t MakeWord(uint8_t h, uint8_t l) { return h << 8 | l; }
bool AudioGeneratorMOD::LoadHeader()
{
uint8_t i;
uint8_t temp[4];
uint8_t junk[22];
if (20 != file->read(/*Mod.name*/junk, 20)) return false; // Skip MOD name
for (i = 0; i < SAMPLES; i++) {
if (22 != file->read(junk /*Mod.samples[i].name*/, 22)) return false; // Skip sample name
if (2 != file->read(temp, 2)) return false;
Mod.samples[i].length = MakeWord(temp[0], temp[1]) * 2;
if (1 != file->read(reinterpret_cast<uint8_t*>(&Mod.samples[i].fineTune), 1)) return false;
if (Mod.samples[i].fineTune > 7) Mod.samples[i].fineTune -= 16;
if (1 != file->read(&Mod.samples[i].volume, 1)) return false;
if (2 != file->read(temp, 2)) return false;
Mod.samples[i].loopBegin = MakeWord(temp[0], temp[1]) * 2;
if (2 != file->read(temp, 2)) return false;
Mod.samples[i].loopLength = MakeWord(temp[0], temp[1]) * 2;
if (Mod.samples[i].loopBegin + Mod.samples[i].loopLength > Mod.samples[i].length)
Mod.samples[i].loopLength = Mod.samples[i].length - Mod.samples[i].loopBegin;
}
if (1 != file->read(&Mod.songLength, 1)) return false;
if (1 != file->read(temp, 1)) return false; // Discard this byte
Mod.numberOfPatterns = 0;
for (i = 0; i < 128; i++) {
if (1 != file->read(&Mod.order[i], 1)) return false;
if (Mod.order[i] > Mod.numberOfPatterns)
Mod.numberOfPatterns = Mod.order[i];
}
Mod.numberOfPatterns++;
// Offset 1080
if (4 != file->read(temp, 4)) return false;;
if (!strncmp(reinterpret_cast<const char*>(temp + 1), "CHN", 3))
Mod.numberOfChannels = temp[0] - '0';
else if (!strncmp(reinterpret_cast<const char*>(temp + 2), "CH", 2))
Mod.numberOfChannels = (temp[0] - '0') * 10 + temp[1] - '0';
else
Mod.numberOfChannels = 4;
return true;
}
void AudioGeneratorMOD::LoadSamples()
{
uint8_t i;
uint32_t fileOffset = 1084 + Mod.numberOfPatterns * ROWS * Mod.numberOfChannels * 4 - 1;
for (i = 0; i < SAMPLES; i++) {
if (Mod.samples[i].length) {
Mixer.sampleBegin[i] = fileOffset;
Mixer.sampleEnd[i] = fileOffset + Mod.samples[i].length;
if (Mod.samples[i].loopLength > 2) {
Mixer.sampleloopBegin[i] = fileOffset + Mod.samples[i].loopBegin;
Mixer.sampleLoopLength[i] = Mod.samples[i].loopLength;
Mixer.sampleLoopEnd[i] = Mixer.sampleloopBegin[i] + Mixer.sampleLoopLength[i];
} else {
Mixer.sampleloopBegin[i] = 0;
Mixer.sampleLoopLength[i] = 0;
Mixer.sampleLoopEnd[i] = 0;
}
fileOffset += Mod.samples[i].length;
}
}
}
bool AudioGeneratorMOD::LoadPattern(uint8_t pattern)
{
uint8_t row;
uint8_t channel;
uint8_t i;
uint8_t temp[4];
uint16_t amigaPeriod;
if (!file->seek(1084 + pattern * ROWS * Mod.numberOfChannels * 4, SEEK_SET)) return false;
for (row = 0; row < ROWS; row++) {
for (channel = 0; channel < Mod.numberOfChannels; channel++) {
if (4 != file->read(temp, 4)) return false;
Player.currentPattern.sampleNumber[row][channel] = (temp[0] & 0xF0) + (temp[2] >> 4);
amigaPeriod = ((temp[0] & 0xF) << 8) + temp[1];
// Player.currentPattern.note[row][channel] = NONOTE;
Player.currentPattern.note8[row][channel] = NONOTE8;
for (i = 1; i < 37; i++)
if (amigaPeriod > ReadAmigaPeriods(i * 8) - 3 &&
amigaPeriod < ReadAmigaPeriods(i * 8) + 3)
Player.currentPattern.note8[row][channel] = i;
Player.currentPattern.effectNumber[row][channel] = temp[2] & 0xF;
Player.currentPattern.effectParameter[row][channel] = temp[3];
}
}
return true;
}
void AudioGeneratorMOD::Portamento(uint8_t channel)
{
if (Player.lastAmigaPeriod[channel] < Player.portamentoNote[channel]) {
Player.lastAmigaPeriod[channel] += Player.portamentoSpeed[channel];
if (Player.lastAmigaPeriod[channel] > Player.portamentoNote[channel])
Player.lastAmigaPeriod[channel] = Player.portamentoNote[channel];
}
if (Player.lastAmigaPeriod[channel] > Player.portamentoNote[channel]) {
Player.lastAmigaPeriod[channel] -= Player.portamentoSpeed[channel];
if (Player.lastAmigaPeriod[channel] < Player.portamentoNote[channel])
Player.lastAmigaPeriod[channel] = Player.portamentoNote[channel];
}
Mixer.channelFrequency[channel] = Player.amiga / Player.lastAmigaPeriod[channel];
}
void AudioGeneratorMOD::Vibrato(uint8_t channel)
{
uint16_t delta;
uint16_t temp;
temp = Player.vibratoPos[channel] & 31;
switch (Player.waveControl[channel] & 3) {
case 0:
delta = ReadSine(temp);
break;
case 1:
temp <<= 3;
if (Player.vibratoPos[channel] < 0)
temp = 255 - temp;
delta = temp;
break;
case 2:
delta = 255;
break;
case 3:
delta = rand() & 255;
break;
}
delta *= Player.vibratoDepth[channel];
delta >>= 7;
if (Player.vibratoPos[channel] >= 0)
Mixer.channelFrequency[channel] = Player.amiga / (Player.lastAmigaPeriod[channel] + delta);
else
Mixer.channelFrequency[channel] = Player.amiga / (Player.lastAmigaPeriod[channel] - delta);
Player.vibratoPos[channel] += Player.vibratoSpeed[channel];
if (Player.vibratoPos[channel] > 31) Player.vibratoPos[channel] -= 64;
}
void AudioGeneratorMOD::Tremolo(uint8_t channel)
{
uint16_t delta;
uint16_t temp;
temp = Player.tremoloPos[channel] & 31;
switch (Player.waveControl[channel] & 3) {
case 0:
delta = ReadSine(temp);
break;
case 1:
temp <<= 3;
if (Player.tremoloPos[channel] < 0)
temp = 255 - temp;
delta = temp;
break;
case 2:
delta = 255;
break;
case 3:
delta = rand() & 255;
break;
}
delta *= Player.tremoloDepth[channel];
delta >>= 6;
if (Player.tremoloPos[channel] >= 0) {
if (Player.volume[channel] + delta > 64) delta = 64 - Player.volume[channel];
Mixer.channelVolume[channel] = Player.volume[channel] + delta;
} else {
if (Player.volume[channel] - delta < 0) delta = Player.volume[channel];
Mixer.channelVolume[channel] = Player.volume[channel] - delta;
}
Player.tremoloPos[channel] += Player.tremoloSpeed[channel];
if (Player.tremoloPos[channel] > 31) Player.tremoloPos[channel] -= 64;
}
bool AudioGeneratorMOD::ProcessRow()
{
bool jumpFlag;
bool breakFlag;
uint8_t channel;
uint8_t sampleNumber;
uint16_t note;
uint8_t effectNumber;
uint8_t effectParameter;
uint8_t effectParameterX;
uint8_t effectParameterY;
uint16_t sampleOffset;
if (!running) return false;
Player.lastRow = Player.row++;
jumpFlag = false;
breakFlag = false;
for (channel = 0; channel < Mod.numberOfChannels; channel++) {
sampleNumber = Player.currentPattern.sampleNumber[Player.lastRow][channel];
note = NOTE(Player.lastRow, channel);
effectNumber = Player.currentPattern.effectNumber[Player.lastRow][channel];
effectParameter = Player.currentPattern.effectParameter[Player.lastRow][channel];
effectParameterX = effectParameter >> 4;
effectParameterY = effectParameter & 0xF;
sampleOffset = 0;
if (sampleNumber) {
Player.lastSampleNumber[channel] = sampleNumber - 1;
if (!(effectParameter == 0xE && effectParameterX == NOTEDELAY))
Player.volume[channel] = Mod.samples[Player.lastSampleNumber[channel]].volume;
}
if (note != NONOTE) {
Player.lastNote[channel] = note;
Player.amigaPeriod[channel] = ReadAmigaPeriods(note + Mod.samples[Player.lastSampleNumber[channel]].fineTune);
if (effectNumber != TONEPORTAMENTO && effectNumber != PORTAMENTOVOLUMESLIDE)
Player.lastAmigaPeriod[channel] = Player.amigaPeriod[channel];
if (!(Player.waveControl[channel] & 0x80)) Player.vibratoPos[channel] = 0;
if (!(Player.waveControl[channel] & 0x08)) Player.tremoloPos[channel] = 0;
}
switch (effectNumber) {
case TONEPORTAMENTO:
if (effectParameter) Player.portamentoSpeed[channel] = effectParameter;
Player.portamentoNote[channel] = Player.amigaPeriod[channel];
note = NONOTE;
break;
case VIBRATO:
if (effectParameterX) Player.vibratoSpeed[channel] = effectParameterX;
if (effectParameterY) Player.vibratoDepth[channel] = effectParameterY;
break;
case PORTAMENTOVOLUMESLIDE:
Player.portamentoNote[channel] = Player.amigaPeriod[channel];
note = NONOTE;
break;
case TREMOLO:
if (effectParameterX) Player.tremoloSpeed[channel] = effectParameterX;
if (effectParameterY) Player.tremoloDepth[channel] = effectParameterY;
break;
case SETCHANNELPANNING:
Mixer.channelPanning[channel] = effectParameter >> 1;
break;
case SETSAMPLEOFFSET:
sampleOffset = effectParameter << 8;
if (sampleOffset > Mod.samples[Player.lastSampleNumber[channel]].length)
sampleOffset = Mod.samples[Player.lastSampleNumber[channel]].length;
break;
case JUMPTOORDER:
Player.orderIndex = effectParameter;
if (Player.orderIndex >= Mod.songLength)
Player.orderIndex = 0;
Player.row = 0;
jumpFlag = true;
break;
case SETVOLUME:
if (effectParameter > 64) Player.volume[channel] = 64;
else Player.volume[channel] = effectParameter;
break;
case BREAKPATTERNTOROW:
Player.row = effectParameterX * 10 + effectParameterY;
if (Player.row >= ROWS)
Player.row = 0;
if (!jumpFlag && !breakFlag) {
Player.orderIndex++;
if (Player.orderIndex >= Mod.songLength)
Player.orderIndex = 0;
}
breakFlag = true;
break;
case 0xE:
switch (effectParameterX) {
case FINEPORTAMENTOUP:
Player.lastAmigaPeriod[channel] -= effectParameterY;
break;
case FINEPORTAMENTODOWN:
Player.lastAmigaPeriod[channel] += effectParameterY;
break;
case SETVIBRATOWAVEFORM:
Player.waveControl[channel] &= 0xF0;
Player.waveControl[channel] |= effectParameterY;
break;
case SETFINETUNE:
Mod.samples[Player.lastSampleNumber[channel]].fineTune = effectParameterY;
if (Mod.samples[Player.lastSampleNumber[channel]].fineTune > 7)
Mod.samples[Player.lastSampleNumber[channel]].fineTune -= 16;
break;
case PATTERNLOOP:
if (effectParameterY) {
if (Player.patternLoopCount[channel])
Player.patternLoopCount[channel]--;
else
Player.patternLoopCount[channel] = effectParameterY;
if (Player.patternLoopCount[channel])
Player.row = Player.patternLoopRow[channel] - 1;
} else
Player.patternLoopRow[channel] = Player.row;
break;
case SETTREMOLOWAVEFORM:
Player.waveControl[channel] &= 0xF;
Player.waveControl[channel] |= effectParameterY << 4;
break;
case FINEVOLUMESLIDEUP:
Player.volume[channel] += effectParameterY;
if (Player.volume[channel] > 64) Player.volume[channel] = 64;
break;
case FINEVOLUMESLIDEDOWN:
Player.volume[channel] -= effectParameterY;
if (Player.volume[channel] < 0) Player.volume[channel] = 0;
break;
case NOTECUT:
note = NONOTE;
break;
case PATTERNDELAY:
Player.patternDelay = effectParameterY;
break;
case INVERTLOOP:
break;
}
break;
case SETSPEED:
if (effectParameter < 0x20)
Player.speed = effectParameter;
else
Player.samplesPerTick = sampleRate / (2 * effectParameter / 5);
break;
}
if (note != NONOTE || (Player.lastAmigaPeriod[channel] &&
effectNumber != VIBRATO && effectNumber != VIBRATOVOLUMESLIDE &&
!(effectNumber == 0xE && effectParameterX == NOTEDELAY)))
Mixer.channelFrequency[channel] = Player.amiga / Player.lastAmigaPeriod[channel];
if (note != NONOTE)
Mixer.channelSampleOffset[channel] = sampleOffset << DIVIDER;
if (sampleNumber)
Mixer.channelSampleNumber[channel] = Player.lastSampleNumber[channel];
if (effectNumber != TREMOLO)
Mixer.channelVolume[channel] = Player.volume[channel];
}
return true;
}
bool AudioGeneratorMOD::ProcessTick()
{
uint8_t channel;
uint8_t sampleNumber;
uint16_t note;
uint8_t effectNumber;
uint8_t effectParameter;
uint8_t effectParameterX;
uint8_t effectParameterY;
uint16_t tempNote;
if (!running) return false;
for (channel = 0; channel < Mod.numberOfChannels; channel++) {
if (Player.lastAmigaPeriod[channel]) {
sampleNumber = Player.currentPattern.sampleNumber[Player.lastRow][channel];
// note = Player.currentPattern.note[Player.lastRow][channel];
note = NOTE(Player.lastRow, channel);
effectNumber = Player.currentPattern.effectNumber[Player.lastRow][channel];
effectParameter = Player.currentPattern.effectParameter[Player.lastRow][channel];
effectParameterX = effectParameter >> 4;
effectParameterY = effectParameter & 0xF;
switch (effectNumber) {
case ARPEGGIO:
if (effectParameter)
switch (Player.tick % 3) {
case 0:
Mixer.channelFrequency[channel] = Player.amiga / Player.lastAmigaPeriod[channel];
break;
case 1:
tempNote = Player.lastNote[channel] + effectParameterX * 8 + Mod.samples[Player.lastSampleNumber[channel]].fineTune;
if (tempNote < 296) Mixer.channelFrequency[channel] = Player.amiga / ReadAmigaPeriods(tempNote);
break;
case 2:
tempNote = Player.lastNote[channel] + effectParameterY * 8 + Mod.samples[Player.lastSampleNumber[channel]].fineTune;
if (tempNote < 296) Mixer.channelFrequency[channel] = Player.amiga / ReadAmigaPeriods(tempNote);
break;
}
break;
case PORTAMENTOUP:
Player.lastAmigaPeriod[channel] -= effectParameter;
if (Player.lastAmigaPeriod[channel] < 113) Player.lastAmigaPeriod[channel] = 113;
Mixer.channelFrequency[channel] = Player.amiga / Player.lastAmigaPeriod[channel];
break;
case PORTAMENTODOWN:
Player.lastAmigaPeriod[channel] += effectParameter;
if (Player.lastAmigaPeriod[channel] > 856) Player.lastAmigaPeriod[channel] = 856;
Mixer.channelFrequency[channel] = Player.amiga / Player.lastAmigaPeriod[channel];
break;
case TONEPORTAMENTO:
Portamento(channel);
break;
case VIBRATO:
Vibrato(channel);
break;
case PORTAMENTOVOLUMESLIDE:
Portamento(channel);
Player.volume[channel] += effectParameterX - effectParameterY;
if (Player.volume[channel] < 0) Player.volume[channel] = 0;
else if (Player.volume[channel] > 64) Player.volume[channel] = 64;
Mixer.channelVolume[channel] = Player.volume[channel];
break;
case VIBRATOVOLUMESLIDE:
Vibrato(channel);
Player.volume[channel] += effectParameterX - effectParameterY;
if (Player.volume[channel] < 0) Player.volume[channel] = 0;
else if (Player.volume[channel] > 64) Player.volume[channel] = 64;
Mixer.channelVolume[channel] = Player.volume[channel];
break;
case TREMOLO:
Tremolo(channel);
break;
case VOLUMESLIDE:
Player.volume[channel] += effectParameterX - effectParameterY;
if (Player.volume[channel] < 0) Player.volume[channel] = 0;
else if (Player.volume[channel] > 64) Player.volume[channel] = 64;
Mixer.channelVolume[channel] = Player.volume[channel];
break;
case 0xE:
switch (effectParameterX) {
case RETRIGGERNOTE:
if (!effectParameterY) break;
if (!(Player.tick % effectParameterY)) {
Mixer.channelSampleOffset[channel] = 0;
}
break;
case NOTECUT:
if (Player.tick == effectParameterY)
Mixer.channelVolume[channel] = Player.volume[channel] = 0;
break;
case NOTEDELAY:
if (Player.tick == effectParameterY) {
if (sampleNumber) Player.volume[channel] = Mod.samples[Player.lastSampleNumber[channel]].volume;
if (note != NONOTE) Mixer.channelSampleOffset[channel] = 0;
Mixer.channelFrequency[channel] = Player.amiga / Player.lastAmigaPeriod[channel];
Mixer.channelVolume[channel] = Player.volume[channel];
}
break;
}
break;
}
}
}
return true;
}
bool AudioGeneratorMOD::RunPlayer()
{
if (!running) return false;
if (Player.tick == Player.speed) {
Player.tick = 0;
if (Player.row == ROWS) {
Player.orderIndex++;
if (Player.orderIndex == Mod.songLength)
{
//Player.orderIndex = 0;
// No loop, just say we're done!
return false;
}
Player.row = 0;
}
if (Player.patternDelay) {
Player.patternDelay--;
} else {
if (Player.orderIndex != Player.oldOrderIndex)
if (!LoadPattern(Mod.order[Player.orderIndex])) return false;
Player.oldOrderIndex = Player.orderIndex;
if (!ProcessRow()) return false;
}
} else {
if (!ProcessTick()) return false;
}
Player.tick++;
return true;
}
void AudioGeneratorMOD::GetSample(int16_t sample[2])
{
int16_t sumL;
int16_t sumR;
uint8_t channel;
uint32_t samplePointer;
int8_t current;
int8_t next;
int16_t out;
if (!running) return;
sumL = 0;
sumR = 0;
for (channel = 0; channel < Mod.numberOfChannels; channel++) {
if (!Mixer.channelFrequency[channel] ||
!Mod.samples[Mixer.channelSampleNumber[channel]].length) continue;
Mixer.channelSampleOffset[channel] += Mixer.channelFrequency[channel];
if (!Mixer.channelVolume[channel]) continue;
samplePointer = Mixer.sampleBegin[Mixer.channelSampleNumber[channel]] +
(Mixer.channelSampleOffset[channel] >> DIVIDER);
if (Mixer.sampleLoopLength[Mixer.channelSampleNumber[channel]]) {
if (samplePointer >= Mixer.sampleLoopEnd[Mixer.channelSampleNumber[channel]]) {
Mixer.channelSampleOffset[channel] -= Mixer.sampleLoopLength[Mixer.channelSampleNumber[channel]] << DIVIDER;
samplePointer -= Mixer.sampleLoopLength[Mixer.channelSampleNumber[channel]];
}
} else {
if (samplePointer >= Mixer.sampleEnd[Mixer.channelSampleNumber[channel]]) {
Mixer.channelFrequency[channel] = 0;
samplePointer = Mixer.sampleEnd[Mixer.channelSampleNumber[channel]];
}
}
if (samplePointer < FatBuffer.samplePointer[channel] ||
samplePointer >= FatBuffer.samplePointer[channel] + fatBufferSize - 1 ||
Mixer.channelSampleNumber[channel] != FatBuffer.channelSampleNumber[channel]) {
uint16_t toRead = Mixer.sampleEnd[Mixer.channelSampleNumber[channel]] - samplePointer + 1;
if (toRead > fatBufferSize) toRead = fatBufferSize;
if (!file->seek(samplePointer, SEEK_SET)) {
stop();
return;
}
if (toRead != file->read(FatBuffer.channels[channel], toRead)) {
stop();
return;
}
FatBuffer.samplePointer[channel] = samplePointer;
FatBuffer.channelSampleNumber[channel] = Mixer.channelSampleNumber[channel];
}
current = FatBuffer.channels[channel][(samplePointer - FatBuffer.samplePointer[channel]) /*& (FATBUFFERSIZE - 1)*/];
next = FatBuffer.channels[channel][(samplePointer + 1 - FatBuffer.samplePointer[channel]) /*& (FATBUFFERSIZE - 1)*/];
out = current;
// Integer linear interpolation
out += (next - current) * (Mixer.channelSampleOffset[channel] & ((1 << DIVIDER) - 1)) >> DIVIDER;
// Upscale to BITDEPTH
out <<= BITDEPTH - 8;
// Channel volume
out = out * Mixer.channelVolume[channel] >> 6;
// Channel panning
sumL += out * min(128 - Mixer.channelPanning[channel], 64) >> 6;
sumR += out * min(Mixer.channelPanning[channel], 64) >> 6;
}
// Downscale to BITDEPTH
sumL /= Mod.numberOfChannels;
sumR /= Mod.numberOfChannels;
// Fill the sound buffer with unsigned values
sample[AudioOutput::LEFTCHANNEL] = sumL + (1 << (BITDEPTH - 1));
sample[AudioOutput::RIGHTCHANNEL] = sumR + (1 << (BITDEPTH - 1));
}
bool AudioGeneratorMOD::LoadMOD()
{
uint8_t channel;
if (!LoadHeader()) return false;
LoadSamples();
Player.amiga = AMIGA;
Player.samplesPerTick = sampleRate / (2 * 125 / 5); // Hz = 2 * BPM / 5
Player.speed = 6;
Player.tick = Player.speed;
Player.row = 0;
Player.orderIndex = 0;
Player.oldOrderIndex = 0xFF;
Player.patternDelay = 0;
for (channel = 0; channel < Mod.numberOfChannels; channel++) {
Player.patternLoopCount[channel] = 0;
Player.patternLoopRow[channel] = 0;
Player.lastAmigaPeriod[channel] = 0;
Player.waveControl[channel] = 0;
Player.vibratoSpeed[channel] = 0;
Player.vibratoDepth[channel] = 0;
Player.vibratoPos[channel] = 0;
Player.tremoloSpeed[channel] = 0;
Player.tremoloDepth[channel] = 0;
Player.tremoloPos[channel] = 0;
FatBuffer.samplePointer[channel] = 0;
FatBuffer.channelSampleNumber[channel] = 0xFF;
Mixer.channelSampleOffset[channel] = 0;
Mixer.channelFrequency[channel] = 0;
Mixer.channelVolume[channel] = 0;
switch (channel % 4) {
case 0:
case 3:
Mixer.channelPanning[channel] = stereoSeparation;
break;
default:
Mixer.channelPanning[channel] = 128 - stereoSeparation;
}
}
return true;
}

View File

@ -0,0 +1,168 @@
/*
AudioGeneratorMOD
Audio output generator that plays Amiga MOD tracker files
Copyright (C) 2017 Earle F. Philhower, III
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef _AUDIOGENERATORMOD_H
#define _AUDIOGENERATORMOD_H
#include "AudioGenerator.h"
class AudioGeneratorMOD : public AudioGenerator
{
public:
AudioGeneratorMOD();
virtual ~AudioGeneratorMOD() override;
virtual bool begin(AudioFileSource *source, AudioOutput *output) override;
virtual bool loop() override;
virtual bool stop() override;
virtual bool isRunning() override { return running; }
bool SetSampleRate(int hz) { if (running || (hz < 1) || (hz > 96000) ) return false; sampleRate = hz; return true; }
bool SetBufferSize(int sz) { if (running || (sz < 1) ) return false; fatBufferSize = sz; return true; }
bool SetStereoSeparation(int sep) { if (running || (sep<0) || (sep>64)) return false; stereoSeparation = sep; return true; }
bool SetPAL(bool use) { if (running) return false; usePAL = use; return true; }
protected:
bool LoadMOD();
bool LoadHeader();
void GetSample(int16_t sample[2]);
bool RunPlayer();
void LoadSamples();
bool LoadPattern(uint8_t pattern);
bool ProcessTick();
bool ProcessRow();
void Tremolo(uint8_t channel);
void Portamento(uint8_t channel);
void Vibrato(uint8_t channel);
protected:
int mixerTick;
enum {BITDEPTH = 15};
int sampleRate;
int fatBufferSize; //(6*1024) // File system buffers per-CHANNEL (i.e. total mem required is 4 * FATBUFFERSIZE)
enum {DIVIDER = 10}; // Fixed-point mantissa used for integer arithmetic
int stereoSeparation; //STEREOSEPARATION = 32; // 0 (max) to 64 (mono)
bool usePAL;
// Hz = 7093789 / (amigaPeriod * 2) for PAL
// Hz = 7159091 / (amigaPeriod * 2) for NTSC
int AMIGA;
void UpdateAmiga() { AMIGA = ((usePAL?7159091:7093789) / 2 / sampleRate << DIVIDER); }
enum {ROWS = 64, SAMPLES = 31, CHANNELS = 4, NONOTE = 0xFFFF, NONOTE8 = 0xff };
typedef struct Sample {
uint16_t length;
int8_t fineTune;
uint8_t volume;
uint16_t loopBegin;
uint16_t loopLength;
} Sample;
typedef struct mod {
Sample samples[SAMPLES];
uint8_t songLength;
uint8_t numberOfPatterns;
uint8_t order[128];
uint8_t numberOfChannels;
} mod;
// Save 256 bytes by storing raw note values, unpack with macro NOTE
typedef struct Pattern {
uint8_t sampleNumber[ROWS][CHANNELS];
uint8_t note8[ROWS][CHANNELS];
uint8_t effectNumber[ROWS][CHANNELS];
uint8_t effectParameter[ROWS][CHANNELS];
} Pattern;
typedef struct player {
Pattern currentPattern;
uint32_t amiga;
uint16_t samplesPerTick;
uint8_t speed;
uint8_t tick;
uint8_t row;
uint8_t lastRow;
uint8_t orderIndex;
uint8_t oldOrderIndex;
uint8_t patternDelay;
uint8_t patternLoopCount[CHANNELS];
uint8_t patternLoopRow[CHANNELS];
uint8_t lastSampleNumber[CHANNELS];
int8_t volume[CHANNELS];
uint16_t lastNote[CHANNELS];
uint16_t amigaPeriod[CHANNELS];
int16_t lastAmigaPeriod[CHANNELS];
uint16_t portamentoNote[CHANNELS];
uint8_t portamentoSpeed[CHANNELS];
uint8_t waveControl[CHANNELS];
uint8_t vibratoSpeed[CHANNELS];
uint8_t vibratoDepth[CHANNELS];
int8_t vibratoPos[CHANNELS];
uint8_t tremoloSpeed[CHANNELS];
uint8_t tremoloDepth[CHANNELS];
int8_t tremoloPos[CHANNELS];
} player;
typedef struct mixer {
uint32_t sampleBegin[SAMPLES];
uint32_t sampleEnd[SAMPLES];
uint32_t sampleloopBegin[SAMPLES];
uint16_t sampleLoopLength[SAMPLES];
uint32_t sampleLoopEnd[SAMPLES];
uint8_t channelSampleNumber[CHANNELS];
uint32_t channelSampleOffset[CHANNELS];
uint16_t channelFrequency[CHANNELS];
uint8_t channelVolume[CHANNELS];
uint8_t channelPanning[CHANNELS];
} mixer;
typedef struct fatBuffer {
uint8_t *channels[CHANNELS]; // Make dynamically allocated [FATBUFFERSIZE];
uint32_t samplePointer[CHANNELS];
uint8_t channelSampleNumber[CHANNELS];
} fatBuffer;
// Effects
typedef enum { ARPEGGIO = 0, PORTAMENTOUP, PORTAMENTODOWN, TONEPORTAMENTO, VIBRATO, PORTAMENTOVOLUMESLIDE,
VIBRATOVOLUMESLIDE, TREMOLO, SETCHANNELPANNING, SETSAMPLEOFFSET, VOLUMESLIDE, JUMPTOORDER,
SETVOLUME, BREAKPATTERNTOROW, ESUBSET, SETSPEED } EffectsValues;
// 0xE subset
typedef enum { SETFILTER = 0, FINEPORTAMENTOUP, FINEPORTAMENTODOWN, GLISSANDOCONTROL, SETVIBRATOWAVEFORM,
SETFINETUNE, PATTERNLOOP, SETTREMOLOWAVEFORM, SUBEFFECT8, RETRIGGERNOTE, FINEVOLUMESLIDEUP,
FINEVOLUMESLIDEDOWN, NOTECUT, NOTEDELAY, PATTERNDELAY, INVERTLOOP } Effect08Subvalues;
// Our state lives here...
player Player;
mod Mod;
mixer Mixer;
fatBuffer FatBuffer;
};
#endif

View File

@ -0,0 +1,353 @@
/*
AudioGeneratorMP3
Wrap libmad MP3 library to play audio
Copyright (C) 2017 Earle F. Philhower, III
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "AudioGeneratorMP3.h"
AudioGeneratorMP3::AudioGeneratorMP3()
{
running = false;
file = NULL;
output = NULL;
buff = NULL;
nsCountMax = 1152/32;
madInitted = false;
preallocateSpace = NULL;
preallocateSize = 0;
}
AudioGeneratorMP3::AudioGeneratorMP3(void *space, int size)
{
running = false;
file = NULL;
output = NULL;
buff = NULL;
nsCountMax = 1152/32;
madInitted = false;
preallocateSpace = space;
preallocateSize = size;
}
AudioGeneratorMP3::~AudioGeneratorMP3()
{
if (!preallocateSpace) {
free(buff);
free(synth);
free(frame);
free(stream);
}
}
bool AudioGeneratorMP3::stop()
{
if (madInitted) {
mad_synth_finish(synth);
mad_frame_finish(frame);
mad_stream_finish(stream);
madInitted = false;
}
if (!preallocateSpace) {
free(buff);
free(synth);
free(frame);
free(stream);
}
buff = NULL;
synth = NULL;
frame = NULL;
stream = NULL;
running = false;
output->stop();
return file->close();
}
bool AudioGeneratorMP3::isRunning()
{
return running;
}
enum mad_flow AudioGeneratorMP3::ErrorToFlow()
{
char err[64];
char errLine[128];
// Special case - eat "lost sync @ byte 0" as it always occurs and is not really correct....it never had sync!
if ((lastReadPos==0) && (stream->error==MAD_ERROR_LOSTSYNC)) return MAD_FLOW_CONTINUE;
strcpy_P(err, mad_stream_errorstr(stream));
snprintf_P(errLine, sizeof(errLine), PSTR("Decoding error '%s' at byte offset %d"),
err, (stream->this_frame - buff) + lastReadPos);
yield(); // Something bad happened anyway, ensure WiFi gets some time, too
cb.st(stream->error, errLine);
return MAD_FLOW_CONTINUE;
}
enum mad_flow AudioGeneratorMP3::Input()
{
int unused = 0;
if (stream->next_frame) {
unused = lastBuffLen - (stream->next_frame - buff);
memmove(buff, stream->next_frame, unused);
stream->next_frame = NULL;
}
if (unused == lastBuffLen) {
// Something wicked this way came, throw it all out and try again
unused = 0;
}
lastReadPos = file->getPos() - unused;
int len = buffLen - unused;
len = file->read(buff + unused, len);
if ((len == 0) && (unused == 0)) {
// Can't read any from the file, and we don't have anything left. It's done....
return MAD_FLOW_STOP;
}
lastBuffLen = len + unused;
mad_stream_buffer(stream, buff, lastBuffLen);
return MAD_FLOW_CONTINUE;
}
bool AudioGeneratorMP3::DecodeNextFrame()
{
if (mad_frame_decode(frame, stream) == -1) {
ErrorToFlow(); // Always returns CONTINUE
return false;
}
nsCountMax = MAD_NSBSAMPLES(&frame->header);
return true;
}
bool AudioGeneratorMP3::GetOneSample(int16_t sample[2])
{
if (synth->pcm.samplerate != lastRate) {
output->SetRate(synth->pcm.samplerate);
lastRate = synth->pcm.samplerate;
}
if (synth->pcm.channels != lastChannels) {
output->SetChannels(synth->pcm.channels);
lastChannels = synth->pcm.channels;
}
// If we're here, we have one decoded frame and sent 0 or more samples out
if (samplePtr < synth->pcm.length) {
sample[AudioOutput::LEFTCHANNEL ] = synth->pcm.samples[0][samplePtr];
sample[AudioOutput::RIGHTCHANNEL] = synth->pcm.samples[1][samplePtr];
samplePtr++;
} else {
samplePtr = 0;
switch ( mad_synth_frame_onens(synth, frame, nsCount++) ) {
case MAD_FLOW_STOP:
case MAD_FLOW_BREAK: audioLogger->printf_P(PSTR("msf1ns failed\n"));
return false; // Either way we're done
default:
break; // Do nothing
}
// for IGNORE and CONTINUE, just play what we have now
sample[AudioOutput::LEFTCHANNEL ] = synth->pcm.samples[0][samplePtr];
sample[AudioOutput::RIGHTCHANNEL] = synth->pcm.samples[1][samplePtr];
samplePtr++;
}
return true;
}
bool AudioGeneratorMP3::loop()
{
if (!running) goto done; // Nothing to do here!
// First, try and push in the stored sample. If we can't, then punt and try later
if (!output->ConsumeSample(lastSample)) goto done; // Can't send, but no error detected
// Try and stuff the buffer one sample at a time
do
{
// Decode next frame if we're beyond the existing generated data
if ( (samplePtr >= synth->pcm.length) && (nsCount >= nsCountMax) ) {
retry:
if (Input() == MAD_FLOW_STOP) {
return false;
}
if (!DecodeNextFrame()) {
goto retry;
}
samplePtr = 9999;
nsCount = 0;
}
if (!GetOneSample(lastSample)) {
audioLogger->printf_P(PSTR("G1S failed\n"));
running = false;
goto done;
}
} while (running && output->ConsumeSample(lastSample));
done:
file->loop();
output->loop();
return running;
}
bool AudioGeneratorMP3::begin(AudioFileSource *source, AudioOutput *output)
{
if (!source) return false;
file = source;
if (!output) return false;
this->output = output;
if (!file->isOpen()) {
audioLogger->printf_P(PSTR("MP3 source file not open\n"));
return false; // Error
}
output->SetBitsPerSample(16); // Constant for MP3 decoder
output->SetChannels(2);
if (!output->begin()) return false;
// Where we are in generating one frame's data, set to invalid so we will run loop on first getsample()
samplePtr = 9999;
nsCount = 9999;
lastRate = 0;
lastChannels = 0;
lastReadPos = 0;
lastBuffLen = 0;
// Allocate all large memory chunks
if (preallocateSpace) {
uint8_t *p = reinterpret_cast<uint8_t *>(preallocateSpace);
buff = reinterpret_cast<unsigned char *>(p);
p += (buffLen+7) & ~7;
stream = reinterpret_cast<struct mad_stream *>(p);
p += (sizeof(struct mad_stream)+7) & ~7;
frame = reinterpret_cast<struct mad_frame *>(p);
p += (sizeof(struct mad_frame)+7) & ~7;
synth = reinterpret_cast<struct mad_synth *>(p);
p += (sizeof(struct mad_synth)+7) & ~7;
int neededBytes = p - reinterpret_cast<uint8_t *>(preallocateSpace);
if (neededBytes > preallocateSize) {
audioLogger->printf_P("OOM error in MP3: Want %d bytes, have %d bytes preallocated.\n", neededBytes, preallocateSize);
return false;
}
} else {
buff = reinterpret_cast<unsigned char *>(malloc(buffLen));
stream = reinterpret_cast<struct mad_stream *>(malloc(sizeof(struct mad_stream)));
frame = reinterpret_cast<struct mad_frame *>(malloc(sizeof(struct mad_frame)));
synth = reinterpret_cast<struct mad_synth *>(malloc(sizeof(struct mad_synth)));
if (!buff || !stream || !frame || !synth) {
free(buff);
free(stream);
free(frame);
free(synth);
buff = NULL;
stream = NULL;
frame = NULL;
synth = NULL;
uint32_t size = buffLen + sizeof(struct mad_stream) + sizeof(struct mad_frame) + sizeof(struct mad_synth);
audioLogger->printf_P("OOM error in MP3: Want %d bytes\n", size);
return false;
}
}
mad_stream_init(stream);
mad_frame_init(frame);
mad_synth_init(synth);
synth->pcm.length = 0;
mad_stream_options(stream, 0); // TODO - add options support
madInitted = true;
running = true;
return true;
}
// The following are helper routines for use in libmad to check stack/heap free
// and to determine if there's enough stack space to allocate some blocks there
// instead of precious heap.
#undef stack
extern "C" {
#ifdef ESP32
//TODO - add ESP32 checks
void stack(const char *s, const char *t, int i)
{
}
int stackfree()
{
return 8192;
}
#elif defined(ESP8266)
#include <cont.h>
extern cont_t g_cont;
void stack(const char *s, const char *t, int i)
{
(void) t;
(void) i;
register uint32_t *sp asm("a1");
int freestack = 4 * (sp - g_cont.stack);
int freeheap = ESP.getFreeHeap();
if ((freestack < 512) || (freeheap < 5120)) {
static int laststack, lastheap;
if (laststack!=freestack|| lastheap !=freeheap) {
audioLogger->printf_P(PSTR("%s: FREESTACK=%d, FREEHEAP=%d\n"), s, /*t, i,*/ freestack, /*cont_get_free_stack(&g_cont),*/ freeheap);
}
if (freestack < 256) {
audioLogger->printf_P(PSTR("out of stack!\n"));
}
if (freeheap < 1024) {
audioLogger->printf_P(PSTR("out of heap!\n"));
}
Serial.flush();
laststack = freestack;
lastheap = freeheap;
}
}
int stackfree()
{
register uint32_t *sp asm("a1");
int freestack = 4 * (sp - g_cont.stack);
return freestack;
}
#else
void stack(const char *s, const char *t, int i)
{
(void) s;
(void) t;
(void) i;
}
int stackfree()
{
return 8192;
}
#endif
}

View File

@ -0,0 +1,68 @@
/*
AudioGeneratorMP3
Wrap libmad MP3 library to play audio
Copyright (C) 2017 Earle F. Philhower, III
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef _AUDIOGENERATORMP3_H
#define _AUDIOGENERATORMP3_H
#include "AudioGenerator.h"
#include "libmad/config.h"
#include "libmad/mad.h"
class AudioGeneratorMP3 : public AudioGenerator
{
public:
AudioGeneratorMP3();
AudioGeneratorMP3(void *preallocateSpace, int preallocateSize);
virtual ~AudioGeneratorMP3() override;
virtual bool begin(AudioFileSource *source, AudioOutput *output) override;
virtual bool loop() override;
virtual bool stop() override;
virtual bool isRunning() override;
protected:
void *preallocateSpace;
int preallocateSize;
const int buffLen = 0x600; // Slightly larger than largest MP3 frame
unsigned char *buff;
int lastReadPos;
int lastBuffLen;
unsigned int lastRate;
int lastChannels;
// Decoding bits
bool madInitted;
struct mad_stream *stream;
struct mad_frame *frame;
struct mad_synth *synth;
int samplePtr;
int nsCount;
int nsCountMax;
// The internal helpers
enum mad_flow ErrorToFlow();
enum mad_flow Input();
bool DecodeNextFrame();
bool GetOneSample(int16_t sample[2]);
};
#endif

View File

@ -0,0 +1,162 @@
/*
AudioGeneratorMP3
Audio output generator using the Helix MP3 decoder
Copyright (C) 2017 Earle F. Philhower, III
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma GCC optimize ("O3")
#include "AudioGeneratorMP3a.h"
AudioGeneratorMP3a::AudioGeneratorMP3a()
{
running = false;
file = NULL;
output = NULL;
hMP3Decoder = MP3InitDecoder();
if (!hMP3Decoder) {
audioLogger->printf_P(PSTR("Out of memory error! hMP3Decoder==NULL\n"));
Serial.flush();
}
// For sanity's sake...
memset(buff, 0, sizeof(buff));
memset(outSample, 0, sizeof(outSample));
buffValid = 0;
lastFrameEnd = 0;
validSamples = 0;
curSample = 0;
lastRate = 0;
lastChannels = 0;
}
AudioGeneratorMP3a::~AudioGeneratorMP3a()
{
MP3FreeDecoder(hMP3Decoder);
}
bool AudioGeneratorMP3a::stop()
{
if (!running) return true;
running = false;
output->stop();
return file->close();
}
bool AudioGeneratorMP3a::isRunning()
{
return running;
}
bool AudioGeneratorMP3a::FillBufferWithValidFrame()
{
buff[0] = 0; // Destroy any existing sync word @ 0
int nextSync;
do {
nextSync = MP3FindSyncWord(buff + lastFrameEnd, buffValid - lastFrameEnd);
if (nextSync >= 0) nextSync += lastFrameEnd;
lastFrameEnd = 0;
if (nextSync == -1) {
if (buff[buffValid-1]==0xff) { // Could be 1st half of syncword, preserve it...
buff[0] = 0xff;
buffValid = file->read(buff+1, sizeof(buff)-1);
if (buffValid==0) return false; // No data available, EOF
} else { // Try a whole new buffer
buffValid = file->read(buff, sizeof(buff));
if (buffValid==0) return false; // No data available, EOF
}
}
} while (nextSync == -1);
// Move the frame to start at offset 0 in the buffer
buffValid -= nextSync; // Throw out prior to nextSync
memmove(buff, buff+nextSync, buffValid);
// We have a sync word at 0 now, try and fill remainder of buffer
buffValid += file->read(buff + buffValid, sizeof(buff) - buffValid);
return true;
}
bool AudioGeneratorMP3a::loop()
{
if (!running) goto done; // Nothing to do here!
// If we've got data, try and pump it out...
while (validSamples) {
lastSample[0] = outSample[curSample*2];
lastSample[1] = outSample[curSample*2 + 1];
if (!output->ConsumeSample(lastSample)) goto done; // Can't send, but no error detected
validSamples--;
curSample++;
}
// No samples available, need to decode a new frame
if (FillBufferWithValidFrame()) {
// buff[0] start of frame, decode it...
unsigned char *inBuff = reinterpret_cast<unsigned char *>(buff);
int bytesLeft = buffValid;
int ret = MP3Decode(hMP3Decoder, &inBuff, &bytesLeft, outSample, 0);
if (ret) {
// Error, skip the frame...
char buff[48];
sprintf(buff, "MP3 decode error %d", ret);
cb.st(ret, buff);
} else {
lastFrameEnd = buffValid - bytesLeft;
MP3FrameInfo fi;
MP3GetLastFrameInfo(hMP3Decoder, &fi);
if ((int)fi.samprate!= (int)lastRate) {
output->SetRate(fi.samprate);
lastRate = fi.samprate;
}
if (fi.nChans != lastChannels) {
output->SetChannels(fi.nChans);
lastChannels = fi.nChans;
}
curSample = 0;
validSamples = fi.outputSamps / lastChannels;
}
} else {
running = false; // No more data, we're done here...
}
done:
file->loop();
output->loop();
return running;
}
bool AudioGeneratorMP3a::begin(AudioFileSource *source, AudioOutput *output)
{
if (!source) return false;
file = source;
if (!output) return false;
this->output = output;
if (!file->isOpen()) return false; // Error
output->begin();
// AAC always comes out at 16 bits
output->SetBitsPerSample(16);
running = true;
return true;
}

View File

@ -0,0 +1,57 @@
/*
AudioGeneratorMP3
Audio output generator using the Helix MP3 decoder
Copyright (C) 2017 Earle F. Philhower, III
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef _AUDIOGENERATORMP3A_H
#define _AUDIOGENERATORMP3A_H
#include "AudioGenerator.h"
#include "libhelix-mp3/mp3dec.h"
class AudioGeneratorMP3a : public AudioGenerator
{
public:
AudioGeneratorMP3a();
virtual ~AudioGeneratorMP3a() override;
virtual bool begin(AudioFileSource *source, AudioOutput *output) override;
virtual bool loop() override;
virtual bool stop() override;
virtual bool isRunning() override;
protected:
// Helix MP3 decoder
HMP3Decoder hMP3Decoder;
// Input buffering
uint8_t buff[1600]; // File buffer required to store at least a whole compressed frame
int16_t buffValid;
int16_t lastFrameEnd;
bool FillBufferWithValidFrame(); // Read until we get a valid syncword and min(feof, 2048) butes in the buffer
// Output buffering
int16_t outSample[1152 * 2]; // Interleaved L/R
int16_t validSamples;
int16_t curSample;
// Each frame may change this if they're very strange, I guess
unsigned int lastRate;
int lastChannels;
};
#endif

View File

@ -0,0 +1,142 @@
/*
AudioGeneratorOpus
Audio output generator that plays Opus audio files
Copyright (C) 2020 Earle F. Philhower, III
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <AudioGeneratorOpus.h>
AudioGeneratorOpus::AudioGeneratorOpus()
{
of = nullptr;
buff = nullptr;
buffPtr = 0;
buffLen = 0;
running = false;
}
AudioGeneratorOpus::~AudioGeneratorOpus()
{
if (of) op_free(of);
of = nullptr;
free(buff);
buff = nullptr;
}
#define OPUS_BUFF 1024
bool AudioGeneratorOpus::begin(AudioFileSource *source, AudioOutput *output)
{
buff = (int16_t*)malloc(OPUS_BUFF * sizeof(int16_t));
if (!buff) return false;
if (!source) return false;
file = source;
if (!output) return false;
this->output = output;
if (!file->isOpen()) return false; // Error
of = op_open_callbacks((void*)this, &cb, nullptr, 0, nullptr);
if (!of) return false;
prev_li = -1;
lastSample[0] = 0;
lastSample[1] = 0;
buffPtr = 0;
buffLen = 0;
output->begin();
// These are fixed by Opus
output->SetRate(48000);
output->SetBitsPerSample(16);
output->SetChannels(2);
running = true;
return true;
}
bool AudioGeneratorOpus::loop()
{
if (!running) goto done;
if (!output->ConsumeSample(lastSample)) goto done; // Try and send last buffered sample
do {
if (buffPtr == buffLen) {
int ret = op_read_stereo(of, (opus_int16 *)buff, OPUS_BUFF);
if (ret == OP_HOLE) {
// fprintf(stderr,"\nHole detected! Corrupt file segment?\n");
continue;
} else if (ret <= 0) {
running = false;
goto done;
}
buffPtr = 0;
buffLen = ret * 2;
}
lastSample[AudioOutput::LEFTCHANNEL] = buff[buffPtr] & 0xffff;
lastSample[AudioOutput::RIGHTCHANNEL] = buff[buffPtr+1] & 0xffff;
buffPtr += 2;
} while (running && output->ConsumeSample(lastSample));
done:
file->loop();
output->loop();
return running;
}
bool AudioGeneratorOpus::stop()
{
if (of) op_free(of);
of = nullptr;
free(buff);
buff = nullptr;
running = false;
output->stop();
return true;
}
bool AudioGeneratorOpus::isRunning()
{
return running;
}
int AudioGeneratorOpus::read_cb(unsigned char *_ptr, int _nbytes) {
if (_nbytes == 0) return 0;
_nbytes = file->read(_ptr, _nbytes);
if (_nbytes == 0) return -1;
return _nbytes;
}
int AudioGeneratorOpus::seek_cb(opus_int64 _offset, int _whence) {
if (!file->seek((int32_t)_offset, _whence)) return -1;
return 0;
}
opus_int64 AudioGeneratorOpus::tell_cb() {
return file->getPos();
}
int AudioGeneratorOpus::close_cb() {
// NO OP, we close in main loop
return 0;
}

View File

@ -0,0 +1,70 @@
/*
AudioGeneratorOpus
Audio output generator that plays Opus audio files
Copyright (C) 2020 Earle F. Philhower, III
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef _AUDIOGENERATOROPUS_H
#define _AUDIOGENERATOROPUS_H
#include <AudioGenerator.h>
//#include "libopus/opus.h"
#include "opusfile/opusfile.h"
class AudioGeneratorOpus : public AudioGenerator
{
public:
AudioGeneratorOpus();
virtual ~AudioGeneratorOpus() override;
virtual bool begin(AudioFileSource *source, AudioOutput *output) override;
virtual bool loop() override;
virtual bool stop() override;
virtual bool isRunning() override;
protected:
// Opus callbacks, need static functions to bounce into C++ from C
static int OPUS_read(void *_stream, unsigned char *_ptr, int _nbytes) {
return static_cast<AudioGeneratorOpus*>(_stream)->read_cb(_ptr, _nbytes);
}
static int OPUS_seek(void *_stream, opus_int64 _offset, int _whence) {
return static_cast<AudioGeneratorOpus*>(_stream)->seek_cb(_offset, _whence);
}
static opus_int64 OPUS_tell(void *_stream) {
return static_cast<AudioGeneratorOpus*>(_stream)->tell_cb();
}
static int OPUS_close(void *_stream) {
return static_cast<AudioGeneratorOpus*>(_stream)->close_cb();
}
// Actual Opus callbacks
int read_cb(unsigned char *_ptr, int _nbytes);
int seek_cb(opus_int64 _offset, int _whence);
opus_int64 tell_cb();
int close_cb();
private:
OpusFileCallbacks cb = {OPUS_read, OPUS_seek, OPUS_tell, OPUS_close};
OggOpusFile *of;
int prev_li; // To detect changes in streams
int16_t *buff;
uint32_t buffPtr;
uint32_t buffLen;
};
#endif

View File

@ -0,0 +1,292 @@
/*
AudioGeneratorRTTTL
Audio output generator that plays RTTTL (Nokia ringtone)
Based on the Rtttl Arduino library by James BM, https://github.com/spicajames/Rtttl
Based on the gist from Daniel Hall https://gist.github.com/smarthall/1618800
Copyright (C) 2018 Earle F. Philhower, III
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "AudioGeneratorRTTTL.h"
AudioGeneratorRTTTL::AudioGeneratorRTTTL()
{
running = false;
file = NULL;
output = NULL;
rate = 22050;
buff = nullptr;
ptr = 0;
}
AudioGeneratorRTTTL::~AudioGeneratorRTTTL()
{
free(buff);
}
bool AudioGeneratorRTTTL::stop()
{
if (!running) return true;
running = false;
output->stop();
return file->close();
}
bool AudioGeneratorRTTTL::isRunning()
{
return running;
}
bool AudioGeneratorRTTTL::loop()
{
if (!running) goto done; // Nothing to do here!
// Load the next note, if we've hit the end of the last one
if (samplesSent == ttlSamples) {
if (!GetNextNote()) {
running = false;
goto done;
}
samplesSent = 0;
}
// Try and send out the remainder of the existing note, one per loop()
if (ttlSamplesPerWaveFP10 == 0) { // Mute
int16_t mute[2] = {0, 0};
while ((samplesSent < ttlSamples) && output->ConsumeSample(mute)) {
samplesSent++;
}
} else {
while (samplesSent < ttlSamples) {
int samplesSentFP10 = samplesSent << 10;
int rem = samplesSentFP10 % ttlSamplesPerWaveFP10;
int16_t val = (rem > ttlSamplesPerWaveFP10/2) ? 8192:-8192;
int16_t s[2] = { val, val };
if (!output->ConsumeSample(s)) goto done;
samplesSent++;
}
}
done:
file->loop();
output->loop();
return running;
}
bool AudioGeneratorRTTTL::SkipWhitespace()
{
while ((ptr < len) && (buff[ptr] == ' ')) ptr++;
return ptr < len;
}
bool AudioGeneratorRTTTL::ReadInt(int *dest)
{
if (ptr >= len) return false;
SkipWhitespace();
if (ptr >= len) return false;
if ((buff[ptr] <'0') || (buff[ptr] > '9')) return false;
int t = 0;
while ((buff[ptr] >= '0') && (buff[ptr] <='9')) {
t = (t * 10) + (buff[ptr] - '0');
ptr++;
}
*dest = t;
return true;
}
bool AudioGeneratorRTTTL::ParseHeader()
{
// Skip the title
while ((ptr < len) && (buff[ptr] != ':')) ptr++;
if (ptr >= len) return false;
if (buff[ptr++] != ':') return false;
if (!SkipWhitespace()) return false;
if ((buff[ptr] != 'd') && (buff[ptr] != 'D')) return false;
ptr++;
if (!SkipWhitespace()) return false;
if (buff[ptr++] != '=') return false;
if (!ReadInt(&defaultDuration)) return false;
if (!SkipWhitespace()) return false;
if (buff[ptr++] != ',') return false;
if (!SkipWhitespace()) return false;
if ((buff[ptr] != 'o') && (buff[ptr] != 'O')) return false;
ptr++;
if (!SkipWhitespace()) return false;
if (buff[ptr++] != '=') return false;
if (!ReadInt(&defaultOctave)) return false;
if (!SkipWhitespace()) return false;
if (buff[ptr++] != ',') return false;
int bpm;
if (!SkipWhitespace()) return false;
if ((buff[ptr] != 'b') && (buff[ptr] != 'B')) return false;
ptr++;
if (!SkipWhitespace()) return false;
if (buff[ptr++] != '=') return false;
if (!ReadInt(&bpm)) return false;
if (!SkipWhitespace()) return false;
if (buff[ptr++] != ':') return false;
wholeNoteMS = (60 * 1000 * 4) / bpm;
return true;
}
#define NOTE_C4 262
#define NOTE_CS4 277
#define NOTE_D4 294
#define NOTE_DS4 311
#define NOTE_E4 330
#define NOTE_F4 349
#define NOTE_FS4 370
#define NOTE_G4 392
#define NOTE_GS4 415
#define NOTE_A4 440
#define NOTE_AS4 466
#define NOTE_B4 494
#define NOTE_C5 523
#define NOTE_CS5 554
#define NOTE_D5 587
#define NOTE_DS5 622
#define NOTE_E5 659
#define NOTE_F5 698
#define NOTE_FS5 740
#define NOTE_G5 784
#define NOTE_GS5 831
#define NOTE_A5 880
#define NOTE_AS5 932
#define NOTE_B5 988
#define NOTE_C6 1047
#define NOTE_CS6 1109
#define NOTE_D6 1175
#define NOTE_DS6 1245
#define NOTE_E6 1319
#define NOTE_F6 1397
#define NOTE_FS6 1480
#define NOTE_G6 1568
#define NOTE_GS6 1661
#define NOTE_A6 1760
#define NOTE_AS6 1865
#define NOTE_B6 1976
#define NOTE_C7 2093
#define NOTE_CS7 2217
#define NOTE_D7 2349
#define NOTE_DS7 2489
#define NOTE_E7 2637
#define NOTE_F7 2794
#define NOTE_FS7 2960
#define NOTE_G7 3136
#define NOTE_GS7 3322
#define NOTE_A7 3520
#define NOTE_AS7 3729
#define NOTE_B7 3951
static int notes[49] = { 0,
NOTE_C4, NOTE_CS4, NOTE_D4, NOTE_DS4, NOTE_E4, NOTE_F4, NOTE_FS4, NOTE_G4, NOTE_GS4, NOTE_A4, NOTE_AS4, NOTE_B4,
NOTE_C5, NOTE_CS5, NOTE_D5, NOTE_DS5, NOTE_E5, NOTE_F5, NOTE_FS5, NOTE_G5, NOTE_GS5, NOTE_A5, NOTE_AS5, NOTE_B5,
NOTE_C6, NOTE_CS6, NOTE_D6, NOTE_DS6, NOTE_E6, NOTE_F6, NOTE_FS6, NOTE_G6, NOTE_GS6, NOTE_A6, NOTE_AS6, NOTE_B6,
NOTE_C7, NOTE_CS7, NOTE_D7, NOTE_DS7, NOTE_E7, NOTE_F7, NOTE_FS7, NOTE_G7, NOTE_GS7, NOTE_A7, NOTE_AS7, NOTE_B7 };
bool AudioGeneratorRTTTL::GetNextNote()
{
int dur, note, scale;
if (ptr >= len) return false;
if (!ReadInt(&dur)) {
dur = defaultDuration;
}
dur = wholeNoteMS / dur;
if (ptr >= len) return false;
note = 0;
switch (buff[ptr++]) {
case 'c': case 'C': note = 1; break;
case 'd': case 'D': note = 3; break;
case 'e': case 'E': note = 5; break;
case 'f': case 'F': note = 6; break;
case 'g': case 'G': note = 8; break;
case 'a': case 'A': note = 10; break;
case 'b': case 'B': note = 12; break;
case 'p': case 'P': note = 0; break;
default: return false;
}
if ((ptr < len) && (buff[ptr] == '#')) {
ptr++;
note++;
}
if ((ptr < len) && (buff[ptr] == '.')) {
ptr++;
dur += dur / 2;
}
if (!ReadInt(&scale)) {
scale = defaultOctave;
}
// Eat any trailing whitespace and comma
SkipWhitespace();
if ((ptr < len) && (buff[ptr]==',')) {
ptr++;
}
if (scale < 4) scale = 4;
if (scale > 7) scale = 7;
if (note) {
int freq = notes[(scale - 4) * 12 + note];
// Convert from frequency in Hz to high and low samples in fixed point
ttlSamplesPerWaveFP10 = (rate << 10) / freq;
} else {
ttlSamplesPerWaveFP10 = 0;
}
ttlSamples = (rate * dur ) / 1000;
//audioLogger->printf("%d %d %d %d %d\n", dur, note, scale, ttlSamplesPerWaveFP10, ttlSamples );
return true;
}
bool AudioGeneratorRTTTL::begin(AudioFileSource *source, AudioOutput *output)
{
if (!source) return false;
file = source;
if (!output) return false;
this->output = output;
if (!file->isOpen()) return false; // Error
len = file->getSize();
buff = (char *)malloc(len);
if (!buff) return false;
if (file->read(buff, len) != (uint32_t)len) return false;
ptr = 0;
samplesSent = 0;
ttlSamples = 0;
if (!ParseHeader()) return false;
if (!output->SetRate( rate )) return false;
if (!output->SetBitsPerSample( 16 )) return false;
if (!output->SetChannels( 2 )) return false;
if (!output->begin()) return false;
running = true;
return true;
}

View File

@ -0,0 +1,66 @@
/*
AudioGeneratorRTTTL
Audio output generator that plays RTTTL (Nokia ringtones)
Based on the Rtttl Arduino library by James BM, https://github.com/spicajames/Rtttl
Based on the gist from Daniel Hall https://gist.github.com/smarthall/1618800
Copyright (C) 2018 Earle F. Philhower, III
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef _AUDIOGENERATORRTTTL_H
#define _AUDIOGENERATORRTTTL_H
#include "AudioGenerator.h"
class AudioGeneratorRTTTL : public AudioGenerator
{
public:
AudioGeneratorRTTTL();
virtual ~AudioGeneratorRTTTL() override;
virtual bool begin(AudioFileSource *source, AudioOutput *output) override;
virtual bool loop() override;
virtual bool stop() override;
virtual bool isRunning() override;
void SetRate(uint16_t hz) { rate = hz; }
private:
bool SkipWhitespace();
bool ReadInt(int *dest);
bool ParseHeader();
bool GetNextNote();
protected:
uint16_t rate;
// We copy the entire tiny song to a buffer for easier access
char *buff;
int len;
int ptr;
// Song-global settings
int defaultDuration;
int defaultOctave;
int wholeNoteMS;
// The note we're currently playing
int ttlSamplesPerWaveFP10;
int ttlSamples;
int samplesSent;
};
#endif

View File

@ -0,0 +1,302 @@
/*
AudioGeneratorTalkie
Audio output generator that speaks using the LPC code in old TI speech chips
Output is locked at 8khz as that's that the hardcoded LPC coefficients are built around
Based on the Talkie Arduino library by Peter Knight, https://github.com/going-digital/Talkie
Copyright (C) 2020 Earle F. Philhower, III
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "AudioGeneratorTalkie.h"
AudioGeneratorTalkie::AudioGeneratorTalkie()
{
running = false;
lastFrame = false;
file = nullptr;
output = nullptr;
buff = nullptr;
}
AudioGeneratorTalkie::~AudioGeneratorTalkie()
{
free(buff);
}
bool AudioGeneratorTalkie::say(const uint8_t *data, size_t len, bool async) {
// Finish saying anything in the pipe
while (running) {
loop();
delay(0);
}
buff = (uint8_t*)realloc(buff, len);
if (!buff) return false;
memcpy_P(buff, data, len);
// Reset the interpreter to the start of the stream
ptrAddr = buff;
ptrBit = 0;
frameLeft = 0;
lastFrame = false;
running = true;
if (!async) {
// Finish saying anything in the pipe
while (running) {
loop();
delay(0);
}
}
return true;
}
bool AudioGeneratorTalkie::begin(AudioFileSource *source, AudioOutput *output)
{
if (!output) return false;
this->output = output;
if (source) {
file = source;
if (!file->isOpen()) return false; // Error
auto len = file->getSize();
uint8_t *temp = (uint8_t *)malloc(len);
if (!temp) return false;
if (file->read(temp, len) != (uint32_t)len) return false;
say(temp, len);
free(temp);
} else {
// Reset the interpreter to the start of the stream
ptrAddr = buff;
ptrBit = 0;
frameLeft = 0;
running = false;
}
if (!output->SetRate( 8000 )) return false;
if (!output->SetBitsPerSample( 16 )) return false;
if (!output->SetChannels( 2 )) return false;
if (!output->begin()) return false;
return true;
}
bool AudioGeneratorTalkie::stop()
{
if (!running) return true;
running = false;
output->stop();
return file ? file->close() : true;
}
bool AudioGeneratorTalkie::isRunning()
{
return running;
}
bool AudioGeneratorTalkie::loop()
{
if (!running) goto done; // Nothing to do here!
if (!frameLeft) {
if (lastFrame) {
running = false;
goto done;
}
lastFrame = genOneFrame();
}
if (frameLeft) {
for ( ; frameLeft; frameLeft--) {
auto res = genOneSample();
int16_t r[2] = {res, res};
if (!output->ConsumeSample(r)) break;
}
}
done:
if (file) file->loop();
output->loop();
return running;
}
// The ROMs used with the TI speech were serial, not byte wide.
// Here's a handy routine to flip ROM data which is usually reversed.
uint8_t AudioGeneratorTalkie::rev(uint8_t a)
{
// 76543210
a = (a>>4) | (a<<4); // Swap in groups of 4
// 32107654
a = ((a & 0xcc)>>2) | ((a & 0x33)<<2); // Swap in groups of 2
// 10325476
a = ((a & 0xaa)>>1) | ((a & 0x55)<<1); // Swap bit pairs
// 01234567
return a;
}
uint8_t AudioGeneratorTalkie::getBits(uint8_t bits) {
uint8_t value;
uint16_t data;
data = rev(ptrAddr[0])<<8;
if (ptrBit+bits > 8) {
data |= rev(ptrAddr[1]);
}
data <<= ptrBit;
value = data >> (16-bits);
ptrBit += bits;
if (ptrBit >= 8) {
ptrBit -= 8;
ptrAddr++;
}
return value;
}
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wnarrowing"
// Constant LPC coefficient tables
static const uint8_t tmsEnergy[0x10] = {0x00,0x02,0x03,0x04,0x05,0x07,0x0a,0x0f,0x14,0x20,0x29,0x39,0x51,0x72,0xa1,0xff};
static const uint8_t tmsPeriod[0x40] = {0x00,0x10,0x11,0x12,0x13,0x14,0x15,0x16,0x17,0x18,0x19,0x1A,0x1B,0x1C,0x1D,0x1E,0x1F,0x20,0x21,0x22,0x23,0x24,0x25,0x26,0x27,0x28,0x29,0x2A,0x2B,0x2D,0x2F,0x31,0x33,0x35,0x36,0x39,0x3B,0x3D,0x3F,0x42,0x45,0x47,0x49,0x4D,0x4F,0x51,0x55,0x57,0x5C,0x5F,0x63,0x66,0x6A,0x6E,0x73,0x77,0x7B,0x80,0x85,0x8A,0x8F,0x95,0x9A,0xA0};
static const int16_t tmsK1[0x20] = {0x82C0,0x8380,0x83C0,0x8440,0x84C0,0x8540,0x8600,0x8780,0x8880,0x8980,0x8AC0,0x8C00,0x8D40,0x8F00,0x90C0,0x92C0,0x9900,0xA140,0xAB80,0xB840,0xC740,0xD8C0,0xEBC0,0x0000,0x1440,0x2740,0x38C0,0x47C0,0x5480,0x5EC0,0x6700,0x6D40};
static const int16_t tmsK2[0x20] = {0xAE00,0xB480,0xBB80,0xC340,0xCB80,0xD440,0xDDC0,0xE780,0xF180,0xFBC0,0x0600,0x1040,0x1A40,0x2400,0x2D40,0x3600,0x3E40,0x45C0,0x4CC0,0x5300,0x5880,0x5DC0,0x6240,0x6640,0x69C0,0x6CC0,0x6F80,0x71C0,0x73C0,0x7580,0x7700,0x7E80};
static const int8_t tmsK3[0x10] = {0x92,0x9F,0xAD,0xBA,0xC8,0xD5,0xE3,0xF0,0xFE,0x0B,0x19,0x26,0x34,0x41,0x4F,0x5C};
static const int8_t tmsK4[0x10] = {0xAE,0xBC,0xCA,0xD8,0xE6,0xF4,0x01,0x0F,0x1D,0x2B,0x39,0x47,0x55,0x63,0x71,0x7E};
static const int8_t tmsK5[0x10] = {0xAE,0xBA,0xC5,0xD1,0xDD,0xE8,0xF4,0xFF,0x0B,0x17,0x22,0x2E,0x39,0x45,0x51,0x5C};
static const int8_t tmsK6[0x10] = {0xC0,0xCB,0xD6,0xE1,0xEC,0xF7,0x03,0x0E,0x19,0x24,0x2F,0x3A,0x45,0x50,0x5B,0x66};
static const int8_t tmsK7[0x10] = {0xB3,0xBF,0xCB,0xD7,0xE3,0xEF,0xFB,0x07,0x13,0x1F,0x2B,0x37,0x43,0x4F,0x5A,0x66};
static const int8_t tmsK8[0x08] = {0xC0,0xD8,0xF0,0x07,0x1F,0x37,0x4F,0x66};
static const int8_t tmsK9[0x08] = {0xC0,0xD4,0xE8,0xFC,0x10,0x25,0x39,0x4D};
static const int8_t tmsK10[0x08] = {0xCD,0xDF,0xF1,0x04,0x16,0x20,0x3B,0x4D};
// The chirp we active the filter using
static const int8_t chirp[] = {0x00,0x2a,0xd4,0x32,0xb2,0x12,0x25,0x14,0x02,0xe1,0xc5,0x02,0x5f,0x5a,0x05,0x0f,0x26,0xfc,0xa5,0xa5,0xd6,0xdd,0xdc,0xfc,0x25,0x2b,0x22,0x21,0x0f,0xff,0xf8,0xee,0xed,0xef,0xf7,0xf6,0xfa,0x00,0x03,0x02,0x01};
#pragma GCC diagnostic pop
bool AudioGeneratorTalkie::genOneFrame() {
uint8_t energy;
uint8_t repeat;
// Read speech data, processing the variable size frames.
energy = getBits(4);
if (energy == 0) {
// Energy = 0: rest frame
synthEnergy = 0;
} else if (energy == 0xf) {
// Energy = 15: stop frame. Silence the synthesiser.
synthEnergy = 0;
synthK1 = 0;
synthK2 = 0;
synthK3 = 0;
synthK4 = 0;
synthK5 = 0;
synthK6 = 0;
synthK7 = 0;
synthK8 = 0;
synthK9 = 0;
synthK10 = 0;
} else {
synthEnergy = tmsEnergy[energy];
repeat = getBits(1);
synthPeriod = tmsPeriod[getBits(6)];
// A repeat frame uses the last coefficients
if (!repeat) {
// All frames use the first 4 coefficients
synthK1 = tmsK1[getBits(5)];
synthK2 = tmsK2[getBits(5)];
synthK3 = tmsK3[getBits(4)];
synthK4 = tmsK4[getBits(4)];
if (synthPeriod) {
// Voiced frames use 6 extra coefficients.
synthK5 = tmsK5[getBits(4)];
synthK6 = tmsK6[getBits(4)];
synthK7 = tmsK7[getBits(4)];
synthK8 = tmsK8[getBits(3)];
synthK9 = tmsK9[getBits(3)];
synthK10 = tmsK10[getBits(3)];
}
}
}
frameLeft = 8000 / 40;
return (energy == 0xf); // Last frame will return true
}
int16_t AudioGeneratorTalkie::genOneSample()
{
static uint8_t periodCounter;
static int16_t x0,x1,x2,x3,x4,x5,x6,x7,x8,x9;
int16_t u0,u1,u2,u3,u4,u5,u6,u7,u8,u9,u10;
if (synthPeriod) {
// Voiced source
if (periodCounter < synthPeriod) {
periodCounter++;
} else {
periodCounter = 0;
}
if (periodCounter < sizeof(chirp)) {
u10 = ((chirp[periodCounter]) * (uint32_t) synthEnergy) >> 8;
} else {
u10 = 0;
}
} else {
// Unvoiced source
static uint16_t synthRand = 1;
synthRand = (synthRand >> 1) ^ ((synthRand & 1) ? 0xB800 : 0);
u10 = (synthRand & 1) ? synthEnergy : -synthEnergy;
}
// Lattice filter forward path
u9 = u10 - (((int16_t)synthK10*x9) >> 7);
u8 = u9 - (((int16_t)synthK9*x8) >> 7);
u7 = u8 - (((int16_t)synthK8*x7) >> 7);
u6 = u7 - (((int16_t)synthK7*x6) >> 7);
u5 = u6 - (((int16_t)synthK6*x5) >> 7);
u4 = u5 - (((int16_t)synthK5*x4) >> 7);
u3 = u4 - (((int16_t)synthK4*x3) >> 7);
u2 = u3 - (((int16_t)synthK3*x2) >> 7);
u1 = u2 - (((int32_t)synthK2*x1) >> 15);
u0 = u1 - (((int32_t)synthK1*x0) >> 15);
// Output clamp
if (u0 > 511) u0 = 511;
if (u0 < -512) u0 = -512;
// Lattice filter reverse path
x9 = x8 + (((int16_t)synthK9*u8) >> 7);
x8 = x7 + (((int16_t)synthK8*u7) >> 7);
x7 = x6 + (((int16_t)synthK7*u6) >> 7);
x6 = x5 + (((int16_t)synthK6*u5) >> 7);
x5 = x4 + (((int16_t)synthK5*u4) >> 7);
x4 = x3 + (((int16_t)synthK4*u3) >> 7);
x3 = x2 + (((int16_t)synthK3*u2) >> 7);
x2 = x1 + (((int32_t)synthK2*u1) >> 15);
x1 = x0 + (((int32_t)synthK1*u0) >> 15);
x0 = u0;
uint16_t v = u0; // 10 bits
v <<= 6; // Now full 16
return v;
}

View File

@ -0,0 +1,66 @@
/*
AudioGeneratorTalkie
Audio output generator that speaks using the LPC code in old TI speech chips
Output is locked at 8khz as that's that the hardcoded LPC coefficients are built around
Based on the Talkie Arduino library by Peter Knight, https://github.com/going-digital/Talkie
Copyright (C) 2020 Earle F. Philhower, III
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef _AUDIOGENERATORTALKIE_H
#define _AUDIOGENERATORTALKIE_H
#include "AudioGenerator.h"
class AudioGeneratorTalkie : public AudioGenerator
{
public:
AudioGeneratorTalkie();
virtual ~AudioGeneratorTalkie() override;
virtual bool begin(AudioFileSource *source, AudioOutput *output) override;
virtual bool loop() override;
virtual bool stop() override;
virtual bool isRunning() override;
bool say(const uint8_t *data, size_t len, bool async = false);
protected:
// The data stream we're playing
uint8_t *buff;
// Codeword stream handlers
uint8_t *ptrAddr;
uint8_t ptrBit;
bool lastFrame;
bool genOneFrame(); // Fill up one frame's worth of data, returns if this is the last frame
int16_t genOneSample(); // Generate one sample of a frame
// Utilities
uint8_t rev(uint8_t a);
uint8_t getBits(uint8_t bits);
// Synthesizer state
uint8_t synthPeriod;
uint16_t synthEnergy;
int16_t synthK1, synthK2;
int8_t synthK3, synthK4, synthK5, synthK6, synthK7, synthK8, synthK9, synthK10;
int frameLeft;
};
#endif

View File

@ -0,0 +1,316 @@
/*
AudioGeneratorWAV
Audio output generator that reads 8 and 16-bit WAV files
Copyright (C) 2017 Earle F. Philhower, III
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "AudioGeneratorWAV.h"
AudioGeneratorWAV::AudioGeneratorWAV()
{
running = false;
file = NULL;
output = NULL;
buffSize = 128;
buff = NULL;
buffPtr = 0;
buffLen = 0;
}
AudioGeneratorWAV::~AudioGeneratorWAV()
{
free(buff);
buff = NULL;
}
bool AudioGeneratorWAV::stop()
{
if (!running) return true;
running = false;
free(buff);
buff = NULL;
output->stop();
return file->close();
}
bool AudioGeneratorWAV::isRunning()
{
return running;
}
// Handle buffered reading, reload each time we run out of data
bool AudioGeneratorWAV::GetBufferedData(int bytes, void *dest)
{
if (!running) return false; // Nothing to do here!
uint8_t *p = reinterpret_cast<uint8_t*>(dest);
while (bytes--) {
// Potentially load next batch of data...
if (buffPtr >= buffLen) {
buffPtr = 0;
uint32_t toRead = availBytes > buffSize ? buffSize : availBytes;
buffLen = file->read( buff, toRead );
availBytes -= buffLen;
}
if (buffPtr >= buffLen)
return false; // No data left!
*(p++) = buff[buffPtr++];
}
return true;
}
bool AudioGeneratorWAV::loop()
{
if (!running) goto done; // Nothing to do here!
// First, try and push in the stored sample. If we can't, then punt and try later
if (!output->ConsumeSample(lastSample)) goto done; // Can't send, but no error detected
// Try and stuff the buffer one sample at a time
do
{
if (bitsPerSample == 8) {
uint8_t l, r;
if (!GetBufferedData(1, &l)) stop();
if (channels == 2) {
if (!GetBufferedData(1, &r)) stop();
} else {
r = 0;
}
lastSample[AudioOutput::LEFTCHANNEL] = l;
lastSample[AudioOutput::RIGHTCHANNEL] = r;
} else if (bitsPerSample == 16) {
if (!GetBufferedData(2, &lastSample[AudioOutput::LEFTCHANNEL])) stop();
if (channels == 2) {
if (!GetBufferedData(2, &lastSample[AudioOutput::RIGHTCHANNEL])) stop();
} else {
lastSample[AudioOutput::RIGHTCHANNEL] = 0;
}
}
} while (running && output->ConsumeSample(lastSample));
done:
file->loop();
output->loop();
return running;
}
bool AudioGeneratorWAV::ReadWAVInfo()
{
uint32_t u32;
uint16_t u16;
int toSkip;
// WAV specification document:
// https://www.aelius.com/njh/wavemetatools/doc/riffmci.pdf
// Header == "RIFF"
if (!ReadU32(&u32)) {
Serial.printf_P(PSTR("AudioGeneratorWAV::ReadWAVInfo: failed to read WAV data\n"));
return false;
};
if (u32 != 0x46464952) {
Serial.printf_P(PSTR("AudioGeneratorWAV::ReadWAVInfo: cannot read WAV, invalid RIFF header, got: %08X \n"), (uint32_t) u32);
return false;
}
// Skip ChunkSize
if (!ReadU32(&u32)) {
Serial.printf_P(PSTR("AudioGeneratorWAV::ReadWAVInfo: failed to read WAV data\n"));
return false;
};
// Format == "WAVE"
if (!ReadU32(&u32)) {
Serial.printf_P(PSTR("AudioGeneratorWAV::ReadWAVInfo: failed to read WAV data\n"));
return false;
};
if (u32 != 0x45564157) {
Serial.printf_P(PSTR("AudioGeneratorWAV::ReadWAVInfo: cannot read WAV, invalid WAVE header, got: %08X \n"), (uint32_t) u32);
return false;
}
// there might be JUNK or PAD - ignore it by continuing reading until we get to "fmt "
while (1) {
if (!ReadU32(&u32)) {
Serial.printf_P(PSTR("AudioGeneratorWAV::ReadWAVInfo: failed to read WAV data\n"));
return false;
};
if (u32 == 0x20746d66) break; // 'fmt '
};
// subchunk size
if (!ReadU32(&u32)) {
Serial.printf_P(PSTR("AudioGeneratorWAV::ReadWAVInfo: failed to read WAV data\n"));
return false;
};
if (u32 == 16) { toSkip = 0; }
else if (u32 == 18) { toSkip = 18 - 16; }
else if (u32 == 40) { toSkip = 40 - 16; }
else {
Serial.printf_P(PSTR("AudioGeneratorWAV::ReadWAVInfo: cannot read WAV, appears not to be standard PCM \n"));
return false;
} // we only do standard PCM
// AudioFormat
if (!ReadU16(&u16)) {
Serial.printf_P(PSTR("AudioGeneratorWAV::ReadWAVInfo: failed to read WAV data\n"));
return false;
};
if (u16 != 1) {
Serial.printf_P(PSTR("AudioGeneratorWAV::ReadWAVInfo: cannot read WAV, AudioFormat appears not to be standard PCM \n"));
return false;
} // we only do standard PCM
// NumChannels
if (!ReadU16(&channels)) {
Serial.printf_P(PSTR("AudioGeneratorWAV::ReadWAVInfo: failed to read WAV data\n"));
return false;
};
if ((channels<1) || (channels>2)) {
Serial.printf_P(PSTR("AudioGeneratorWAV::ReadWAVInfo: cannot read WAV, only mono and stereo are supported \n"));
return false;
} // Mono or stereo support only
// SampleRate
if (!ReadU32(&sampleRate)) {
Serial.printf_P(PSTR("AudioGeneratorWAV::ReadWAVInfo: failed to read WAV data\n"));
return false;
};
if (sampleRate < 1) {
Serial.printf_P(PSTR("AudioGeneratorWAV::ReadWAVInfo: cannot read WAV, unknown sample rate \n"));
return false;
} // Weird rate, punt. Will need to check w/DAC to see if supported
// Ignore byterate and blockalign
if (!ReadU32(&u32)) {
Serial.printf_P(PSTR("AudioGeneratorWAV::ReadWAVInfo: failed to read WAV data\n"));
return false;
};
if (!ReadU16(&u16)) {
Serial.printf_P(PSTR("AudioGeneratorWAV::ReadWAVInfo: failed to read WAV data\n"));
return false;
};
// Bits per sample
if (!ReadU16(&bitsPerSample)) {
Serial.printf_P(PSTR("AudioGeneratorWAV::ReadWAVInfo: failed to read WAV data\n"));
return false;
};
if ((bitsPerSample!=8) && (bitsPerSample != 16)) {
Serial.printf_P(PSTR("AudioGeneratorWAV::ReadWAVInfo: cannot read WAV, only 8 or 16 bits is supported \n"));
return false;
} // Only 8 or 16 bits
// Skip any extra header
while (toSkip) {
uint8_t ign;
if (!ReadU8(&ign)) {
Serial.printf_P(PSTR("AudioGeneratorWAV::ReadWAVInfo: failed to read WAV data\n"));
return false;
};
toSkip--;
}
// look for data subchunk
do {
// id == "data"
if (!ReadU32(&u32)) {
Serial.printf_P(PSTR("AudioGeneratorWAV::ReadWAVInfo: failed to read WAV data\n"));
return false;
};
if (u32 == 0x61746164) break; // "data"
// Skip size, read until end of chunk
if (!ReadU32(&u32)) {
Serial.printf_P(PSTR("AudioGeneratorWAV::ReadWAVInfo: failed to read WAV data\n"));
return false;
};
if(!file->seek(u32, SEEK_CUR)) {
Serial.printf_P(PSTR("AudioGeneratorWAV::ReadWAVInfo: failed to read WAV data, seek failed\n"));
return false;
}
} while (1);
if (!file->isOpen()) {
Serial.printf_P(PSTR("AudioGeneratorWAV::ReadWAVInfo: cannot read WAV, file is not open\n"));
return false;
};
// Skip size, read until end of file...
if (!ReadU32(&u32)) {
Serial.printf_P(PSTR("AudioGeneratorWAV::ReadWAVInfo: failed to read WAV data\n"));
return false;
};
availBytes = u32;
// Now set up the buffer or fail
buff = reinterpret_cast<uint8_t *>(malloc(buffSize));
if (!buff) {
Serial.printf_P(PSTR("AudioGeneratorWAV::ReadWAVInfo: cannot read WAV, failed to set up buffer \n"));
return false;
};
buffPtr = 0;
buffLen = 0;
return true;
}
bool AudioGeneratorWAV::begin(AudioFileSource *source, AudioOutput *output)
{
if (!source) {
Serial.printf_P(PSTR("AudioGeneratorWAV::begin: failed: invalid source\n"));
return false;
}
file = source;
if (!output) {
Serial.printf_P(PSTR("AudioGeneratorWAV::begin: invalid output\n"));
return false;
}
this->output = output;
if (!file->isOpen()) {
Serial.printf_P(PSTR("AudioGeneratorWAV::begin: file not open\n"));
return false;
} // Error
if (!ReadWAVInfo()) {
Serial.printf_P(PSTR("AudioGeneratorWAV::begin: failed during ReadWAVInfo\n"));
return false;
}
if (!output->SetRate( sampleRate )) {
Serial.printf_P(PSTR("AudioGeneratorWAV::begin: failed to SetRate in output\n"));
return false;
}
if (!output->SetBitsPerSample( bitsPerSample )) {
Serial.printf_P(PSTR("AudioGeneratorWAV::begin: failed to SetBitsPerSample in output\n"));
return false;
}
if (!output->SetChannels( channels )) {
Serial.printf_P(PSTR("AudioGeneratorWAV::begin: failed to SetChannels in output\n"));
return false;
}
if (!output->begin()) {
Serial.printf_P(PSTR("AudioGeneratorWAV::begin: output's begin did not return true\n"));
return false;
}
running = true;
return true;
}

View File

@ -0,0 +1,61 @@
/*
AudioGeneratorWAV
Audio output generator that reads 8 and 16-bit WAV files
Copyright (C) 2017 Earle F. Philhower, III
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef _AUDIOGENERATORWAV_H
#define _AUDIOGENERATORWAV_H
#include "AudioGenerator.h"
class AudioGeneratorWAV : public AudioGenerator
{
public:
AudioGeneratorWAV();
virtual ~AudioGeneratorWAV() override;
virtual bool begin(AudioFileSource *source, AudioOutput *output) override;
virtual bool loop() override;
virtual bool stop() override;
virtual bool isRunning() override;
void SetBufferSize(int sz) { buffSize = sz; }
private:
bool ReadU32(uint32_t *dest) { return file->read(reinterpret_cast<uint8_t*>(dest), 4); }
bool ReadU16(uint16_t *dest) { return file->read(reinterpret_cast<uint8_t*>(dest), 2); }
bool ReadU8(uint8_t *dest) { return file->read(reinterpret_cast<uint8_t*>(dest), 1); }
bool GetBufferedData(int bytes, void *dest);
bool ReadWAVInfo();
protected:
// WAV info
uint16_t channels;
uint32_t sampleRate;
uint16_t bitsPerSample;
uint32_t availBytes;
// We need to buffer some data in-RAM to avoid doing 1000s of small reads
uint32_t buffSize;
uint8_t *buff;
uint16_t buffPtr;
uint16_t buffLen;
};
#endif

View File

@ -0,0 +1,5 @@
#include "AudioLogger.h"
DevNullOut silencedLogger;
Print* audioLogger = &silencedLogger;

View File

@ -0,0 +1,19 @@
#include <Arduino.h>
#ifndef _AUDIOLOGGER_H
#define _AUDIOLOGGER_H
class DevNullOut: public Print
{
public:
virtual size_t write(uint8_t) { return 1; }
};
extern DevNullOut silencedLogger;
// Global `audioLogger` is initialized to &silencedLogger
// It can be initialized anytime to &Serial or any other Print:: derivative instance.
extern Print* audioLogger;
#endif

Some files were not shown because too many files have changed in this diff Show More