Public Access
Add ESPHome display component; ring-buffer driver shared with PlatformIO
Driver (components/el512/el512_panel.*, ESP-IDF only): - DMA ring of 8 x 4 lines (17 KB) refilled from the I2S EOF interrupt (IRAM) instead of a 142 KB full-frame stream; ~107 KB heap freed. - 256-entry lookup table encodes 8 pixels per step. - Back buffer + displayed buffer, swapped at the start of a frame. - PlatformIO builds it via lib_extra_dirs (EL512Core); lib/EL512 keeps the Adafruit GFX front end. ESPHome external component (platform: el512): - DisplayBuffer with optional ENABLE / _LOWPWR pins, set_enabled(), set_low_power(), helper functions for page lambdas. - still_alive_page: plays the Portal end-credits show, then returns to the previous page. Still Alive engine moved to components/el512/still_alive.* with a built-in 5x7 font (from Adafruit GFX, BSD); the PlatformIO demo uses the same code. The importer writes components/el512/still_alive_data.h and stops at 200 s (--until-ms), matching the hand-trimmed data. esphome/el512-panel.yaml: 8 pages (clock, forecast, agenda, dashboard, now playing, Plex, solar, Still Alive), page select, rotation, display and low-power switches, message text. home_assistant/packages/el512_panel.yaml: helper template sensors for forecast, agenda, now playing and Plex streams. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
410885de24
commit
963590cdfe
27 files changed
+2110
-828
No files matched your search
@@ -0,0 +1,10 @@
|
||||
"""Planar EL512.256-H electroluminescent panel (512x256) on an ESP32.
|
||||
|
||||
Driven by I2S0 in parallel mode with DMA; see el512_panel.h for the details.
|
||||
"""
|
||||
|
||||
import esphome.codegen as cg
|
||||
|
||||
CODEOWNERS = ["@mangavalk"]
|
||||
|
||||
el512_ns = cg.esphome_ns.namespace("el512")
|
||||
@@ -0,0 +1,66 @@
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import display
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_LAMBDA, CONF_PAGES
|
||||
|
||||
from . import el512_ns
|
||||
|
||||
DEPENDENCIES = ["esp32"]
|
||||
|
||||
CONF_VCLK_PIN = "vclk_pin"
|
||||
CONF_VID_PIN = "vid_pin"
|
||||
CONF_TVID_PIN = "tvid_pin"
|
||||
CONF_HS_PIN = "hs_pin"
|
||||
CONF_VS_PIN = "vs_pin"
|
||||
CONF_ENABLE_PIN = "enable_pin"
|
||||
CONF_LOW_POWER_PIN = "low_power_pin"
|
||||
CONF_STILL_ALIVE_PAGE = "still_alive_page"
|
||||
|
||||
EL512Display = el512_ns.class_("EL512Display", display.DisplayBuffer)
|
||||
|
||||
# The signals are routed through the GPIO matrix, so any output-capable GPIO
|
||||
# works; the defaults are the wiring in include/pins.h.
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
display.FULL_DISPLAY_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(EL512Display),
|
||||
cv.Optional(CONF_VCLK_PIN, default=18): pins.internal_gpio_output_pin_number,
|
||||
cv.Optional(CONF_VID_PIN, default=19): pins.internal_gpio_output_pin_number,
|
||||
cv.Optional(CONF_TVID_PIN, default=21): pins.internal_gpio_output_pin_number,
|
||||
cv.Optional(CONF_HS_PIN, default=22): pins.internal_gpio_output_pin_number,
|
||||
cv.Optional(CONF_VS_PIN, default=23): pins.internal_gpio_output_pin_number,
|
||||
cv.Optional(CONF_ENABLE_PIN): pins.internal_gpio_output_pin_number,
|
||||
cv.Optional(CONF_LOW_POWER_PIN): pins.internal_gpio_output_pin_number,
|
||||
# Page (from `pages:`) that plays the Portal end-credits show; its
|
||||
# lambda should call it.draw_still_alive() via id(...).
|
||||
cv.Optional(CONF_STILL_ALIVE_PAGE): cv.use_id(display.DisplayPage),
|
||||
}
|
||||
).extend(cv.polling_component_schema("1s")),
|
||||
cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await display.register_display(var, config)
|
||||
|
||||
cg.add(
|
||||
var.set_pins(
|
||||
config[CONF_VCLK_PIN],
|
||||
config[CONF_VID_PIN],
|
||||
config[CONF_TVID_PIN],
|
||||
config[CONF_HS_PIN],
|
||||
config[CONF_VS_PIN],
|
||||
config.get(CONF_ENABLE_PIN, -1),
|
||||
config.get(CONF_LOW_POWER_PIN, -1),
|
||||
)
|
||||
)
|
||||
if CONF_STILL_ALIVE_PAGE in config:
|
||||
page = await cg.get_variable(config[CONF_STILL_ALIVE_PAGE])
|
||||
cg.add(var.set_still_alive_page(page))
|
||||
if CONF_LAMBDA in config:
|
||||
lambda_ = await cg.process_lambda(
|
||||
config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void
|
||||
)
|
||||
cg.add(var.set_writer(lambda_))
|
||||
@@ -0,0 +1,110 @@
|
||||
#include "el512_display.h"
|
||||
|
||||
#include <esp_heap_caps.h>
|
||||
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::el512 {
|
||||
|
||||
static const char *const TAG = "el512";
|
||||
|
||||
void EL512Display::setup() {
|
||||
this->panel_ = new EL512Panel(this->pins_); // NOLINT(cppcoreguidelines-owning-memory)
|
||||
if (!this->panel_->begin()) {
|
||||
ESP_LOGE(TAG, "Panel init failed (out of DMA memory or no free interrupt)");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
// Apply states that switches may have restored before setup ran.
|
||||
this->panel_->setLowPower(this->low_power_);
|
||||
this->panel_->setEnabled(this->enabled_);
|
||||
}
|
||||
|
||||
void EL512Display::loop() {
|
||||
if (this->still_alive_page_ == nullptr || this->panel_ == nullptr || !this->panel_->running())
|
||||
return;
|
||||
const bool active = this->is_still_alive_playing();
|
||||
const uint32_t now = millis();
|
||||
if (active && !this->still_alive_was_active_) {
|
||||
// Remember where we came from, then start the show from the top.
|
||||
this->still_alive_return_ = this->previous_page_ != this->still_alive_page_ ? this->previous_page_ : nullptr;
|
||||
this->still_alive_.start(now);
|
||||
ESP_LOGI(TAG, "Still Alive started; start the song in %u ms", (unsigned) this->still_alive_song_start_ms());
|
||||
}
|
||||
this->still_alive_was_active_ = active;
|
||||
if (!active)
|
||||
return;
|
||||
if (this->still_alive_.finished(now) && this->still_alive_return_ != nullptr) {
|
||||
this->show_page(this->still_alive_return_);
|
||||
this->update();
|
||||
return;
|
||||
}
|
||||
if (this->still_alive_.update(now))
|
||||
this->update();
|
||||
}
|
||||
|
||||
void EL512Display::play_still_alive() {
|
||||
if (this->still_alive_page_ == nullptr)
|
||||
return;
|
||||
if (this->is_still_alive_playing()) {
|
||||
this->still_alive_.start(millis()); // already showing: just restart
|
||||
} else {
|
||||
this->show_page(this->still_alive_page_); // loop() starts it
|
||||
}
|
||||
}
|
||||
|
||||
void EL512Display::draw_still_alive() {
|
||||
if (this->panel_ != nullptr)
|
||||
this->still_alive_.draw(*this->panel_);
|
||||
}
|
||||
|
||||
void EL512Display::update() {
|
||||
if (this->panel_ == nullptr || !this->panel_->running())
|
||||
return;
|
||||
this->do_update_();
|
||||
this->panel_->show();
|
||||
}
|
||||
|
||||
void EL512Display::fill(Color color) {
|
||||
// The panel clear is a memset; with clipping active, fall back to per-pixel.
|
||||
if (this->panel_ == nullptr || this->get_clipping().is_set()) {
|
||||
Display::fill(color);
|
||||
return;
|
||||
}
|
||||
this->panel_->clear(color.is_on());
|
||||
}
|
||||
|
||||
void EL512Display::draw_absolute_pixel_internal(int x, int y, Color color) {
|
||||
if (this->panel_ != nullptr)
|
||||
this->panel_->setPixel(x, y, color.is_on());
|
||||
}
|
||||
|
||||
void EL512Display::set_enabled(bool on) {
|
||||
this->enabled_ = on;
|
||||
if (this->panel_ != nullptr)
|
||||
this->panel_->setEnabled(on);
|
||||
}
|
||||
|
||||
void EL512Display::set_low_power(bool on) {
|
||||
this->low_power_ = on;
|
||||
if (this->panel_ != nullptr)
|
||||
this->panel_->setLowPower(on);
|
||||
}
|
||||
|
||||
void EL512Display::dump_config() {
|
||||
LOG_DISPLAY("", "EL512.256-H", this);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Pins: VCLK %d, VID %d, TVID %d, HS %d, VS %d, ENABLE %d, _LOWPWR %d\n"
|
||||
" Refresh: %.1f Hz, DMA ring %u bytes\n"
|
||||
" Free internal heap: %u bytes",
|
||||
this->pins_.vclk, this->pins_.vid, this->pins_.tvid, this->pins_.hs, this->pins_.vs,
|
||||
this->pins_.enable, this->pins_.lowpwr_n, EL512Panel::refreshHz(),
|
||||
(unsigned) EL512Panel::RING_BYTES,
|
||||
(unsigned) heap_caps_get_free_size(MALLOC_CAP_INTERNAL));
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
if (this->is_failed())
|
||||
ESP_LOGE(TAG, " Init failed");
|
||||
}
|
||||
|
||||
} // namespace esphome::el512
|
||||
@@ -0,0 +1,68 @@
|
||||
#pragma once
|
||||
|
||||
#include "el512_helpers.h"
|
||||
#include "el512_panel.h"
|
||||
#include "esphome/components/display/display_buffer.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "still_alive.h"
|
||||
|
||||
namespace esphome::el512 {
|
||||
|
||||
// ESPHome display for the Planar EL512.256-H. Drawing goes into the panel
|
||||
// driver's back buffer; every update() hands it to the refresh in one piece.
|
||||
class EL512Display : public display::DisplayBuffer {
|
||||
public:
|
||||
void set_pins(int vclk, int vid, int tvid, int hs, int vs, int enable, int low_power) {
|
||||
this->pins_ = {(int8_t) vclk, (int8_t) vid, (int8_t) tvid, (int8_t) hs,
|
||||
(int8_t) vs, (int8_t) enable, (int8_t) low_power};
|
||||
}
|
||||
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void update() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
|
||||
|
||||
void fill(Color color) override;
|
||||
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_BINARY; }
|
||||
|
||||
// Display on/off (ENABLE) and low-power mode (_LOWPWR), e.g. from template switches.
|
||||
void set_enabled(bool on);
|
||||
void set_low_power(bool on);
|
||||
bool is_enabled() const { return this->enabled_; }
|
||||
bool is_low_power() const { return this->low_power_; }
|
||||
bool has_enable_pin() const { return this->pins_.enable >= 0; }
|
||||
bool has_low_power_pin() const { return this->pins_.lowpwr_n >= 0; }
|
||||
|
||||
// Portal end-credits show. Showing this page restarts it; while it is the
|
||||
// active page the display redraws as the show advances, and when it ends the
|
||||
// previous page comes back. Draw it from that page's lambda with
|
||||
// draw_still_alive().
|
||||
void set_still_alive_page(display::DisplayPage *page) { this->still_alive_page_ = page; }
|
||||
void draw_still_alive();
|
||||
// Show the Still Alive page and start the show from the top (also when it is
|
||||
// already playing).
|
||||
void play_still_alive();
|
||||
bool is_still_alive_playing() const {
|
||||
return this->still_alive_page_ != nullptr && this->get_active_page() == this->still_alive_page_;
|
||||
}
|
||||
// When to start the music, in ms after the page was shown.
|
||||
uint32_t still_alive_song_start_ms() const { return el512sa::StillAlivePlayer::songStartMs(); }
|
||||
|
||||
protected:
|
||||
void draw_absolute_pixel_internal(int x, int y, Color color) override;
|
||||
int get_width_internal() override { return EL512Panel::WIDTH; }
|
||||
int get_height_internal() override { return EL512Panel::HEIGHT; }
|
||||
|
||||
EL512Pins pins_{18, 19, 21, 22, 23, -1, -1};
|
||||
EL512Panel *panel_{nullptr};
|
||||
bool enabled_{true};
|
||||
bool low_power_{false};
|
||||
|
||||
display::DisplayPage *still_alive_page_{nullptr};
|
||||
display::DisplayPage *still_alive_return_{nullptr};
|
||||
bool still_alive_was_active_{false};
|
||||
el512sa::StillAlivePlayer still_alive_;
|
||||
};
|
||||
|
||||
} // namespace esphome::el512
|
||||
@@ -0,0 +1,134 @@
|
||||
// Classic 5x7 console font (printable ASCII 32..126), drawn in a 6x8 cell.
|
||||
// Column-major, 5 bytes per character, bit 0 = top row.
|
||||
//
|
||||
// Taken from glcdfont.c of the Adafruit GFX Library
|
||||
// (https://github.com/adafruit/Adafruit-GFX-Library), license below.
|
||||
//
|
||||
// Software License Agreement (BSD License)
|
||||
//
|
||||
// Copyright (c) 2012 Adafruit Industries. All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are met:
|
||||
//
|
||||
// - Redistributions of source code must retain the above copyright notice,
|
||||
// this list of conditions and the following disclaimer.
|
||||
// - Redistributions in binary form must reproduce the above copyright notice,
|
||||
// this list of conditions and the following disclaimer in the documentation
|
||||
// and/or other materials provided with the distribution.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
// POSSIBILITY OF SUCH DAMAGE.
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
static constexpr char EL512_FONT_FIRST = 32;
|
||||
static constexpr char EL512_FONT_LAST = 126;
|
||||
|
||||
static const uint8_t EL512_FONT[] = {
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, // space
|
||||
0x00, 0x00, 0x5F, 0x00, 0x00, // !
|
||||
0x00, 0x07, 0x00, 0x07, 0x00, // "
|
||||
0x14, 0x7F, 0x14, 0x7F, 0x14, // #
|
||||
0x24, 0x2A, 0x7F, 0x2A, 0x12, // $
|
||||
0x23, 0x13, 0x08, 0x64, 0x62, // %
|
||||
0x36, 0x49, 0x56, 0x20, 0x50, // &
|
||||
0x00, 0x08, 0x07, 0x03, 0x00, // '
|
||||
0x00, 0x1C, 0x22, 0x41, 0x00, // (
|
||||
0x00, 0x41, 0x22, 0x1C, 0x00, // )
|
||||
0x2A, 0x1C, 0x7F, 0x1C, 0x2A, // *
|
||||
0x08, 0x08, 0x3E, 0x08, 0x08, // +
|
||||
0x00, 0x80, 0x70, 0x30, 0x00, // ,
|
||||
0x08, 0x08, 0x08, 0x08, 0x08, // -
|
||||
0x00, 0x00, 0x60, 0x60, 0x00, // .
|
||||
0x20, 0x10, 0x08, 0x04, 0x02, // /
|
||||
0x3E, 0x51, 0x49, 0x45, 0x3E, // 0
|
||||
0x00, 0x42, 0x7F, 0x40, 0x00, // 1
|
||||
0x72, 0x49, 0x49, 0x49, 0x46, // 2
|
||||
0x21, 0x41, 0x49, 0x4D, 0x33, // 3
|
||||
0x18, 0x14, 0x12, 0x7F, 0x10, // 4
|
||||
0x27, 0x45, 0x45, 0x45, 0x39, // 5
|
||||
0x3C, 0x4A, 0x49, 0x49, 0x31, // 6
|
||||
0x41, 0x21, 0x11, 0x09, 0x07, // 7
|
||||
0x36, 0x49, 0x49, 0x49, 0x36, // 8
|
||||
0x46, 0x49, 0x49, 0x29, 0x1E, // 9
|
||||
0x00, 0x00, 0x14, 0x00, 0x00, // :
|
||||
0x00, 0x40, 0x34, 0x00, 0x00, // ;
|
||||
0x00, 0x08, 0x14, 0x22, 0x41, // <
|
||||
0x14, 0x14, 0x14, 0x14, 0x14, // =
|
||||
0x00, 0x41, 0x22, 0x14, 0x08, // >
|
||||
0x02, 0x01, 0x59, 0x09, 0x06, // ?
|
||||
0x3E, 0x41, 0x5D, 0x59, 0x4E, // @
|
||||
0x7C, 0x12, 0x11, 0x12, 0x7C, // A
|
||||
0x7F, 0x49, 0x49, 0x49, 0x36, // B
|
||||
0x3E, 0x41, 0x41, 0x41, 0x22, // C
|
||||
0x7F, 0x41, 0x41, 0x41, 0x3E, // D
|
||||
0x7F, 0x49, 0x49, 0x49, 0x41, // E
|
||||
0x7F, 0x09, 0x09, 0x09, 0x01, // F
|
||||
0x3E, 0x41, 0x41, 0x51, 0x73, // G
|
||||
0x7F, 0x08, 0x08, 0x08, 0x7F, // H
|
||||
0x00, 0x41, 0x7F, 0x41, 0x00, // I
|
||||
0x20, 0x40, 0x41, 0x3F, 0x01, // J
|
||||
0x7F, 0x08, 0x14, 0x22, 0x41, // K
|
||||
0x7F, 0x40, 0x40, 0x40, 0x40, // L
|
||||
0x7F, 0x02, 0x1C, 0x02, 0x7F, // M
|
||||
0x7F, 0x04, 0x08, 0x10, 0x7F, // N
|
||||
0x3E, 0x41, 0x41, 0x41, 0x3E, // O
|
||||
0x7F, 0x09, 0x09, 0x09, 0x06, // P
|
||||
0x3E, 0x41, 0x51, 0x21, 0x5E, // Q
|
||||
0x7F, 0x09, 0x19, 0x29, 0x46, // R
|
||||
0x26, 0x49, 0x49, 0x49, 0x32, // S
|
||||
0x03, 0x01, 0x7F, 0x01, 0x03, // T
|
||||
0x3F, 0x40, 0x40, 0x40, 0x3F, // U
|
||||
0x1F, 0x20, 0x40, 0x20, 0x1F, // V
|
||||
0x3F, 0x40, 0x38, 0x40, 0x3F, // W
|
||||
0x63, 0x14, 0x08, 0x14, 0x63, // X
|
||||
0x03, 0x04, 0x78, 0x04, 0x03, // Y
|
||||
0x61, 0x59, 0x49, 0x4D, 0x43, // Z
|
||||
0x00, 0x7F, 0x41, 0x41, 0x41, // [
|
||||
0x02, 0x04, 0x08, 0x10, 0x20, // backslash
|
||||
0x00, 0x41, 0x41, 0x41, 0x7F, // ]
|
||||
0x04, 0x02, 0x01, 0x02, 0x04, // ^
|
||||
0x40, 0x40, 0x40, 0x40, 0x40, // _
|
||||
0x00, 0x03, 0x07, 0x08, 0x00, // `
|
||||
0x20, 0x54, 0x54, 0x78, 0x40, // a
|
||||
0x7F, 0x28, 0x44, 0x44, 0x38, // b
|
||||
0x38, 0x44, 0x44, 0x44, 0x28, // c
|
||||
0x38, 0x44, 0x44, 0x28, 0x7F, // d
|
||||
0x38, 0x54, 0x54, 0x54, 0x18, // e
|
||||
0x00, 0x08, 0x7E, 0x09, 0x02, // f
|
||||
0x18, 0xA4, 0xA4, 0x9C, 0x78, // g
|
||||
0x7F, 0x08, 0x04, 0x04, 0x78, // h
|
||||
0x00, 0x44, 0x7D, 0x40, 0x00, // i
|
||||
0x20, 0x40, 0x40, 0x3D, 0x00, // j
|
||||
0x7F, 0x10, 0x28, 0x44, 0x00, // k
|
||||
0x00, 0x41, 0x7F, 0x40, 0x00, // l
|
||||
0x7C, 0x04, 0x78, 0x04, 0x78, // m
|
||||
0x7C, 0x08, 0x04, 0x04, 0x78, // n
|
||||
0x38, 0x44, 0x44, 0x44, 0x38, // o
|
||||
0xFC, 0x18, 0x24, 0x24, 0x18, // p
|
||||
0x18, 0x24, 0x24, 0x18, 0xFC, // q
|
||||
0x7C, 0x08, 0x04, 0x04, 0x08, // r
|
||||
0x48, 0x54, 0x54, 0x54, 0x24, // s
|
||||
0x04, 0x04, 0x3F, 0x44, 0x24, // t
|
||||
0x3C, 0x40, 0x40, 0x20, 0x7C, // u
|
||||
0x1C, 0x20, 0x40, 0x20, 0x1C, // v
|
||||
0x3C, 0x40, 0x30, 0x40, 0x3C, // w
|
||||
0x44, 0x28, 0x10, 0x28, 0x44, // x
|
||||
0x4C, 0x90, 0x90, 0x90, 0x7C, // y
|
||||
0x44, 0x64, 0x54, 0x4C, 0x44, // z
|
||||
0x00, 0x08, 0x36, 0x41, 0x00, // {
|
||||
0x00, 0x00, 0x77, 0x00, 0x00, // |
|
||||
0x00, 0x41, 0x36, 0x08, 0x00, // }
|
||||
0x02, 0x01, 0x02, 0x04, 0x02, // ~
|
||||
};
|
||||
@@ -0,0 +1,108 @@
|
||||
// Small helpers for page lambdas (available as el512::... in YAML lambdas).
|
||||
#pragma once
|
||||
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/components/display/display.h"
|
||||
|
||||
namespace esphome::el512 {
|
||||
|
||||
// Split the "a|b|c;d|e|f" lists made by the Home Assistant package.
|
||||
inline std::vector<std::string> split(const std::string &s, char sep) {
|
||||
std::vector<std::string> out;
|
||||
if (s.empty())
|
||||
return out;
|
||||
size_t start = 0;
|
||||
while (true) {
|
||||
const size_t end = s.find(sep, start);
|
||||
out.push_back(s.substr(start, end == std::string::npos ? std::string::npos : end - start));
|
||||
if (end == std::string::npos)
|
||||
return out;
|
||||
start = end + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Field i of a "|"-separated entry, or "" if missing.
|
||||
inline std::string field(const std::string &entry, size_t i) {
|
||||
const auto parts = split(entry, '|');
|
||||
return i < parts.size() ? parts[i] : std::string();
|
||||
}
|
||||
|
||||
// Material Design Icons glyph (UTF-8) for a Home Assistant weather condition.
|
||||
inline const char *weather_icon(const std::string &c) {
|
||||
if (c == "clear-night") return "\U000F0594";
|
||||
if (c == "cloudy") return "\U000F0590";
|
||||
if (c == "fog") return "\U000F0591";
|
||||
if (c == "hail") return "\U000F0592";
|
||||
if (c == "lightning") return "\U000F0593";
|
||||
if (c == "lightning-rainy") return "\U000F067E";
|
||||
if (c == "partlycloudy") return "\U000F0595";
|
||||
if (c == "pouring") return "\U000F0596";
|
||||
if (c == "rainy") return "\U000F0597";
|
||||
if (c == "snowy") return "\U000F0598";
|
||||
if (c == "snowy-rainy") return "\U000F067F";
|
||||
if (c == "sunny") return "\U000F0599";
|
||||
if (c == "windy") return "\U000F059D";
|
||||
if (c == "windy-variant") return "\U000F059E";
|
||||
return "\U000F05D6"; // exceptional / unknown
|
||||
}
|
||||
|
||||
// Readable weather condition ("partlycloudy" -> "Partly cloudy").
|
||||
inline std::string weather_text(const std::string &c) {
|
||||
if (c == "clear-night") return "Clear";
|
||||
if (c == "partlycloudy") return "Partly cloudy";
|
||||
if (c == "lightning-rainy") return "Thunderstorm";
|
||||
if (c == "snowy-rainy") return "Sleet";
|
||||
if (c == "windy-variant") return "Windy";
|
||||
if (c == "exceptional") return "Exceptional";
|
||||
if (c.empty()) return "";
|
||||
std::string s = c;
|
||||
s[0] = (char) toupper(s[0]);
|
||||
return s;
|
||||
}
|
||||
|
||||
// Cut text with "..." so it fits in max_width pixels in the given font.
|
||||
inline std::string fit(display::BaseFont *font, const std::string &text, int max_width) {
|
||||
int w, x_off, baseline, h;
|
||||
font->measure(text.c_str(), &w, &x_off, &baseline, &h);
|
||||
if (w <= max_width)
|
||||
return text;
|
||||
std::string s = text;
|
||||
while (!s.empty()) {
|
||||
// Drop a whole UTF-8 character.
|
||||
do {
|
||||
s.pop_back();
|
||||
} while (!s.empty() && (s.back() & 0xC0) == 0x80);
|
||||
const std::string t = s + "...";
|
||||
font->measure(t.c_str(), &w, &x_off, &baseline, &h);
|
||||
if (w <= max_width)
|
||||
return t;
|
||||
}
|
||||
return "...";
|
||||
}
|
||||
|
||||
// 245 -> "4:05", 3725 -> "1:02:05".
|
||||
inline std::string mmss(int seconds) {
|
||||
if (seconds < 0)
|
||||
seconds = 0;
|
||||
char buf[16];
|
||||
if (seconds >= 3600)
|
||||
snprintf(buf, sizeof(buf), "%d:%02d:%02d", seconds / 3600, seconds / 60 % 60, seconds % 60);
|
||||
else
|
||||
snprintf(buf, sizeof(buf), "%d:%02d", seconds / 60, seconds % 60);
|
||||
return buf;
|
||||
}
|
||||
|
||||
// 1250 -> "1250 W", 4200 -> "4.20 kW".
|
||||
inline std::string watts(float w) {
|
||||
char buf[16];
|
||||
if (w >= 1000 || w <= -1000)
|
||||
snprintf(buf, sizeof(buf), "%.2f kW", w / 1000.0f);
|
||||
else
|
||||
snprintf(buf, sizeof(buf), "%.0f W", w);
|
||||
return buf;
|
||||
}
|
||||
|
||||
} // namespace esphome::el512
|
||||
@@ -0,0 +1,335 @@
|
||||
#include "el512_panel.h"
|
||||
|
||||
#include <driver/gpio.h>
|
||||
#include <esp_attr.h>
|
||||
#include <esp_heap_caps.h>
|
||||
#include <esp_intr_alloc.h>
|
||||
#include <esp_rom_gpio.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
#include <soc/gpio_sig_map.h>
|
||||
#include <soc/i2s_reg.h>
|
||||
#include <soc/i2s_struct.h>
|
||||
#include <soc/periph_defs.h>
|
||||
#include <string.h>
|
||||
|
||||
#if __has_include(<esp_private/periph_ctrl.h>)
|
||||
#include <esp_private/periph_ctrl.h>
|
||||
#else
|
||||
#include <driver/periph_ctrl.h>
|
||||
#endif
|
||||
#if __has_include(<esp32/rom/lldesc.h>)
|
||||
#include <esp32/rom/lldesc.h>
|
||||
#else
|
||||
#include <rom/lldesc.h>
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
// I2S0 clock source when clka_en = 0: PLL_D2_CLK, 160 MHz whatever the CPU clock.
|
||||
constexpr uint32_t I2S_SOURCE_CLOCK_HZ = 160000000;
|
||||
constexpr uint32_t I2S_BYTE_RATE_HZ = EL512Panel::PIXEL_CLOCK_HZ * EL512Panel::BYTES_PER_CLOCK;
|
||||
constexpr uint32_t I2S_CLKM_DIV = I2S_SOURCE_CLOCK_HZ / I2S_BYTE_RATE_HZ;
|
||||
static_assert(I2S_SOURCE_CLOCK_HZ % I2S_BYTE_RATE_HZ == 0,
|
||||
"byte rate must divide 160 MHz exactly");
|
||||
static_assert(I2S_CLKM_DIV >= 2 && I2S_CLKM_DIV <= 255, "clkm_div_num out of range");
|
||||
|
||||
// In 8-bit LCD mode I2S0 puts its byte on DATA_OUT16..23 (bitluni ESP32Lib).
|
||||
constexpr uint32_t I2S0_DATA_SIGNAL_BASE = I2S0O_DATA_OUT16_IDX;
|
||||
|
||||
// BCK runs at half the byte rate (measured: 2.5 MHz BCK at 5 MHz bytes), so each
|
||||
// VCLK period spans two identical bytes. Uninverted, the rising edge landed on the
|
||||
// byte-pair boundary and the image jittered; inverted, it sits mid-pair (verified
|
||||
// on the panel).
|
||||
constexpr bool INVERT_VCLK = true;
|
||||
|
||||
static_assert(EL512Panel::LINES_PER_CHUNK * EL512Panel::ROW_BYTES <= 4092, "descriptor too long");
|
||||
static_assert(EL512Panel::ROW_BYTES % 4 == 0, "rows must be whole DMA words");
|
||||
static_assert(EL512Panel::RING_LINES < EL512Panel::LINES_PER_FRAME, "ring longer than a frame");
|
||||
|
||||
constexpr uint16_t FB_ROW_BYTES = EL512Panel::WIDTH / 8;
|
||||
constexpr uint16_t DATA_BYTES = EL512Panel::DATA_CLOCKS_PER_ROW * EL512Panel::BYTES_PER_CLOCK;
|
||||
|
||||
// One framebuffer byte (8 pixels = 4 clocks) -> 8 stream bytes as two DMA words,
|
||||
// already in I2S byte order. Built once in begin(); read from the ISR.
|
||||
DRAM_ATTR uint32_t s_lut[256][2];
|
||||
|
||||
void buildLut() {
|
||||
for (int b = 0; b < 256; b++) {
|
||||
uint8_t bytes[8];
|
||||
for (int c = 0; c < 4; c++) {
|
||||
// Clock c carries x = 2c and 2c+1. The datasheet numbers columns from 1, so
|
||||
// x = 2c is column 2c+1 (odd -> TVID) and x = 2c+1 is column 2c+2 (even -> VID).
|
||||
const uint8_t bits = (uint8_t)(b << (c * 2));
|
||||
uint8_t v = EL512Panel::BIT_HS;
|
||||
if (bits & 0x80) v |= EL512Panel::BIT_TVID;
|
||||
if (bits & 0x40) v |= EL512Panel::BIT_VID;
|
||||
bytes[(2 * c) ^ 2] = v;
|
||||
bytes[(2 * c + 1) ^ 2] = v;
|
||||
}
|
||||
memcpy(s_lut[b], bytes, sizeof(bytes));
|
||||
}
|
||||
}
|
||||
|
||||
inline uint32_t IRAM_ATTR repeat4(uint8_t v) { return v * 0x01010101u; }
|
||||
|
||||
void resetI2S0() {
|
||||
const uint32_t lcReset = I2S_IN_RST_M | I2S_OUT_RST_M | I2S_AHBM_RST_M | I2S_AHBM_FIFO_RST_M;
|
||||
I2S0.lc_conf.val |= lcReset;
|
||||
I2S0.lc_conf.val &= ~lcReset;
|
||||
const uint32_t confReset =
|
||||
I2S_RX_RESET_M | I2S_RX_FIFO_RESET_M | I2S_TX_RESET_M | I2S_TX_FIFO_RESET_M;
|
||||
I2S0.conf.val |= confReset;
|
||||
I2S0.conf.val &= ~confReset;
|
||||
}
|
||||
|
||||
void routeOutput(int8_t pin, uint32_t signal, bool invert) {
|
||||
if (pin < 0) return;
|
||||
esp_rom_gpio_pad_select_gpio(pin);
|
||||
gpio_set_direction((gpio_num_t)pin, GPIO_MODE_OUTPUT);
|
||||
esp_rom_gpio_connect_out_signal(pin, signal, invert, false);
|
||||
}
|
||||
|
||||
void setupGpioOutput(int8_t pin, bool level) {
|
||||
if (pin < 0) return;
|
||||
esp_rom_gpio_pad_select_gpio(pin);
|
||||
gpio_set_direction((gpio_num_t)pin, GPIO_MODE_OUTPUT);
|
||||
gpio_set_level((gpio_num_t)pin, level);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
EL512Panel::EL512Panel(const EL512Pins& pins) : _pins(pins) {}
|
||||
|
||||
EL512Panel::~EL512Panel() { end(); }
|
||||
|
||||
bool EL512Panel::begin() {
|
||||
if (_running) return true;
|
||||
|
||||
setupGpioOutput(_pins.enable, false);
|
||||
setupGpioOutput(_pins.lowpwr_n, true);
|
||||
_enabled = false;
|
||||
_lowPower = false;
|
||||
|
||||
buildLut();
|
||||
// Both framebuffers are read from the ISR, so they must be internal RAM.
|
||||
_fb = (uint8_t*)heap_caps_calloc(1, FRAMEBUFFER_BYTES, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
|
||||
_front = (uint8_t*)heap_caps_calloc(1, FRAMEBUFFER_BYTES, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
|
||||
_ring = (uint8_t*)heap_caps_malloc(RING_BYTES, MALLOC_CAP_DMA | MALLOC_CAP_8BIT);
|
||||
if (!_fb || !_front || !_ring || !startI2S()) {
|
||||
end();
|
||||
return false;
|
||||
}
|
||||
_running = true;
|
||||
setEnabled(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
void EL512Panel::end() {
|
||||
if (_pins.enable >= 0) gpio_set_level((gpio_num_t)_pins.enable, 0);
|
||||
_enabled = false;
|
||||
stopI2S();
|
||||
_running = false;
|
||||
heap_caps_free(_fb);
|
||||
_fb = nullptr;
|
||||
heap_caps_free(_front);
|
||||
_front = nullptr;
|
||||
heap_caps_free(_ring);
|
||||
_ring = nullptr;
|
||||
}
|
||||
|
||||
void EL512Panel::setEnabled(bool on) {
|
||||
_enabled = on;
|
||||
if (_pins.enable >= 0) gpio_set_level((gpio_num_t)_pins.enable, on);
|
||||
}
|
||||
|
||||
void EL512Panel::setLowPower(bool on) {
|
||||
_lowPower = on;
|
||||
if (_pins.lowpwr_n >= 0) gpio_set_level((gpio_num_t)_pins.lowpwr_n, !on);
|
||||
}
|
||||
|
||||
void EL512Panel::setPixel(int16_t x, int16_t y, bool on) {
|
||||
if (!_fb || x < 0 || y < 0 || x >= WIDTH || y >= HEIGHT) return;
|
||||
uint8_t& b = _fb[(y * WIDTH + x) >> 3];
|
||||
const uint8_t mask = 0x80 >> (x & 7);
|
||||
if (on)
|
||||
b |= mask;
|
||||
else
|
||||
b &= ~mask;
|
||||
}
|
||||
|
||||
bool EL512Panel::getPixel(int16_t x, int16_t y) const {
|
||||
if (!_fb || x < 0 || y < 0 || x >= WIDTH || y >= HEIGHT) return false;
|
||||
return _fb[(y * WIDTH + x) >> 3] & (0x80 >> (x & 7));
|
||||
}
|
||||
|
||||
void EL512Panel::clear(bool on) {
|
||||
if (_fb) memset(_fb, on ? 0xFF : 0x00, FRAMEBUFFER_BYTES);
|
||||
}
|
||||
|
||||
void EL512Panel::show() {
|
||||
if (!_fb || !_front) return;
|
||||
if (!_running) {
|
||||
memcpy(_front, _fb, FRAMEBUFFER_BYTES);
|
||||
return;
|
||||
}
|
||||
// The ISR copies _fb to _front just before it encodes line 0 of a frame.
|
||||
_swapPending = true;
|
||||
for (int i = 0; i < 40 && _swapPending; i++) vTaskDelay(1);
|
||||
}
|
||||
|
||||
void IRAM_ATTR EL512Panel::encodeRow(uint16_t line, uint8_t* out) const {
|
||||
// VS is held high during the last blanking line so that the first HS high
|
||||
// time ending after its rising edge is row 0 (datasheet: Video Data Input).
|
||||
const uint32_t vs = repeat4(line == LINES_PER_FRAME - 1 ? BIT_VS : 0);
|
||||
uint32_t* o = (uint32_t*)out;
|
||||
|
||||
if (line >= HEIGHT) {
|
||||
for (uint16_t i = 0; i < ROW_BYTES / 4; i++) o[i] = vs;
|
||||
return;
|
||||
}
|
||||
const uint8_t* src = _front + (size_t)line * FB_ROW_BYTES;
|
||||
for (uint16_t k = 0; k < FB_ROW_BYTES; k++) {
|
||||
o[2 * k] = s_lut[src[k]][0];
|
||||
o[2 * k + 1] = s_lut[src[k]][1];
|
||||
}
|
||||
// Blanking clocks: HS low. Uniform and word-aligned, so no byte swap needed.
|
||||
for (uint16_t i = DATA_BYTES / 4; i < ROW_BYTES / 4; i++) o[i] = vs;
|
||||
}
|
||||
|
||||
void IRAM_ATTR EL512Panel::fillChunk(uint16_t chunk) {
|
||||
uint16_t line = _chunkLine[chunk];
|
||||
uint8_t* out = _ring + (size_t)chunk * LINES_PER_CHUNK * ROW_BYTES;
|
||||
for (uint16_t i = 0; i < LINES_PER_CHUNK; i++) {
|
||||
if (line == 0 && _swapPending) {
|
||||
const uint32_t* s = (const uint32_t*)_fb;
|
||||
uint32_t* d = (uint32_t*)_front;
|
||||
for (size_t w = 0; w < FRAMEBUFFER_BYTES / 4; w++) d[w] = s[w];
|
||||
_swapPending = false;
|
||||
}
|
||||
encodeRow(line, out + (size_t)i * ROW_BYTES);
|
||||
line = (line + 1 == LINES_PER_FRAME) ? 0 : line + 1;
|
||||
}
|
||||
}
|
||||
|
||||
void IRAM_ATTR EL512Panel::isr(void* arg) {
|
||||
auto* self = static_cast<EL512Panel*>(arg);
|
||||
const uint32_t status = I2S0.int_st.val;
|
||||
I2S0.int_clr.val = status;
|
||||
if (!(status & I2S_OUT_EOF_INT_ST_M)) return;
|
||||
|
||||
// The descriptor that just finished plays again RING_LINES lines later.
|
||||
const lldesc_t* done = (const lldesc_t*)I2S0.out_eof_des_addr;
|
||||
const uint16_t chunk = done - (const lldesc_t*)self->_desc;
|
||||
if (chunk >= CHUNK_COUNT) return;
|
||||
uint16_t next = self->_chunkLine[chunk] + RING_LINES;
|
||||
if (next >= LINES_PER_FRAME) next -= LINES_PER_FRAME;
|
||||
self->_chunkLine[chunk] = next;
|
||||
self->fillChunk(chunk);
|
||||
}
|
||||
|
||||
bool EL512Panel::startI2S() {
|
||||
_desc = (lldesc_t*)heap_caps_calloc(CHUNK_COUNT, sizeof(lldesc_t), MALLOC_CAP_DMA);
|
||||
if (!_desc) return false;
|
||||
for (uint16_t i = 0; i < CHUNK_COUNT; i++) {
|
||||
_chunkLine[i] = i * LINES_PER_CHUNK;
|
||||
fillChunk(i);
|
||||
lldesc_t& d = _desc[i];
|
||||
d.size = LINES_PER_CHUNK * ROW_BYTES;
|
||||
d.length = d.size;
|
||||
d.owner = 1;
|
||||
d.eof = 1; // EOF interrupt after every chunk
|
||||
d.buf = _ring + (size_t)i * LINES_PER_CHUNK * ROW_BYTES;
|
||||
d.qe.stqe_next = &_desc[(i + 1) % CHUNK_COUNT];
|
||||
}
|
||||
|
||||
periph_module_enable(PERIPH_I2S0_MODULE);
|
||||
resetI2S0();
|
||||
|
||||
// 8-bit parallel LCD mode (bitluni ESP32Lib setup; the i ^ 2 byte order in
|
||||
// the lookup table depends on these FIFO/channel modes).
|
||||
I2S0.conf2.val = 0;
|
||||
I2S0.conf2.lcd_en = 1;
|
||||
I2S0.conf2.lcd_tx_wrx2_en = 1;
|
||||
I2S0.conf2.lcd_tx_sdx2_en = 0;
|
||||
|
||||
I2S0.sample_rate_conf.val = 0;
|
||||
I2S0.sample_rate_conf.tx_bits_mod = 8;
|
||||
I2S0.sample_rate_conf.tx_bck_div_num = 1;
|
||||
|
||||
// PLL_D2_CLK / (N + b/a) with b = 0: 160 MHz / 16 = 10 MHz bytes -> 5 MHz BCK.
|
||||
I2S0.clkm_conf.val = 0;
|
||||
I2S0.clkm_conf.clka_en = 0;
|
||||
I2S0.clkm_conf.clkm_div_num = I2S_CLKM_DIV;
|
||||
I2S0.clkm_conf.clkm_div_a = 1;
|
||||
I2S0.clkm_conf.clkm_div_b = 0;
|
||||
|
||||
I2S0.fifo_conf.val = 0;
|
||||
I2S0.fifo_conf.tx_fifo_mod_force_en = 1;
|
||||
I2S0.fifo_conf.tx_fifo_mod = 1;
|
||||
I2S0.fifo_conf.tx_data_num = 32;
|
||||
I2S0.fifo_conf.dscr_en = 1;
|
||||
|
||||
I2S0.conf1.val = 0;
|
||||
I2S0.conf1.tx_stop_en = 0;
|
||||
I2S0.conf1.tx_pcm_bypass = 1;
|
||||
|
||||
I2S0.conf_chan.val = 0;
|
||||
I2S0.conf_chan.tx_chan_mod = 1;
|
||||
|
||||
I2S0.conf.tx_right_first = 1;
|
||||
I2S0.conf.tx_msb_right = 0;
|
||||
I2S0.conf.tx_msb_shift = 0;
|
||||
I2S0.conf.tx_mono = 0;
|
||||
I2S0.conf.tx_short_sync = 0;
|
||||
I2S0.timing.val = 0;
|
||||
|
||||
// Stream bit n -> DATA_OUT(16 + n); BCK -> VCLK.
|
||||
const int8_t dataPins[] = {_pins.vid, _pins.tvid, _pins.hs, _pins.vs};
|
||||
for (uint32_t bit = 0; bit < sizeof(dataPins); bit++) {
|
||||
routeOutput(dataPins[bit], I2S0_DATA_SIGNAL_BASE + bit, false);
|
||||
}
|
||||
routeOutput(_pins.vclk, I2S0O_BCK_OUT_IDX, INVERT_VCLK);
|
||||
|
||||
// EOF interrupt refills the ring. IRAM so it keeps running during flash writes.
|
||||
I2S0.int_ena.val = 0;
|
||||
I2S0.int_clr.val = I2S0.int_raw.val;
|
||||
if (esp_intr_alloc(ETS_I2S0_INTR_SOURCE, ESP_INTR_FLAG_IRAM, &EL512Panel::isr, this,
|
||||
&_intr) != ESP_OK) {
|
||||
_intr = nullptr;
|
||||
return false;
|
||||
}
|
||||
I2S0.int_ena.out_eof = 1;
|
||||
|
||||
// Start the DMA; it loops over the chain until stopI2S().
|
||||
I2S0.lc_conf.val = I2S_OUT_DATA_BURST_EN_M | I2S_OUTDSCR_BURST_EN_M;
|
||||
I2S0.out_link.addr = (uint32_t)&_desc[0];
|
||||
I2S0.out_link.start = 1;
|
||||
I2S0.conf.tx_start = 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
void EL512Panel::stopI2S() {
|
||||
if (!_desc) return; // never started
|
||||
|
||||
I2S0.int_ena.val = 0;
|
||||
if (_intr) {
|
||||
esp_intr_free(_intr);
|
||||
_intr = nullptr;
|
||||
}
|
||||
I2S0.conf.tx_start = 0;
|
||||
I2S0.out_link.stop = 1;
|
||||
resetI2S0();
|
||||
periph_module_disable(PERIPH_I2S0_MODULE);
|
||||
|
||||
// Hand the pins back to GPIO and park them low.
|
||||
const int8_t pins[] = {_pins.vclk, _pins.vid, _pins.tvid, _pins.hs, _pins.vs};
|
||||
for (int8_t pin : pins) {
|
||||
if (pin < 0) continue;
|
||||
esp_rom_gpio_connect_out_signal(pin, SIG_GPIO_OUT_IDX, false, false);
|
||||
gpio_set_level((gpio_num_t)pin, 0);
|
||||
}
|
||||
|
||||
heap_caps_free(_desc);
|
||||
_desc = nullptr;
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
// EL512Panel - refresh driver for the Planar EL512.256-H on an ESP32.
|
||||
//
|
||||
// Uses only ESP-IDF APIs, so the same code builds for Arduino/PlatformIO
|
||||
// (lib/EL512) and for the ESPHome component in this folder.
|
||||
//
|
||||
// The panel has no frame memory: it must be refreshed continuously. I2S0 in
|
||||
// 8-bit parallel (LCD) mode streams the signals by DMA from a small ring of
|
||||
// encoded lines (CHUNK_COUNT chunks of LINES_PER_CHUNK lines). When a chunk has
|
||||
// been sent, the DMA EOF interrupt re-encodes it with the lines that come
|
||||
// RING_LINES later, so the ring stays ~1.7 ms ahead of the panel.
|
||||
//
|
||||
// byte bit 0 = VID (even columns 2..512 = x 1, 3, ..., 511)
|
||||
// byte bit 1 = TVID (odd columns 1..511 = x 0, 2, ..., 510)
|
||||
// byte bit 2 = HS (high while the 256 data clocks of a row are sent)
|
||||
// byte bit 3 = VS (marks the top row)
|
||||
// I2S BCK -> VCLK (inverted, see the .cpp)
|
||||
//
|
||||
// BCK rises once per two stream bytes (measured with PCNT), so every VCLK
|
||||
// period is two identical bytes; the I2S byte rate is 10 MHz for a 5 MHz VCLK.
|
||||
// In 8-bit LCD mode the I2S emits each 32-bit DMA word as bytes 2, 3, 0, 1, so
|
||||
// stream byte i is stored at index i ^ 2.
|
||||
//
|
||||
// Panel in two-bit parallel mode (PS1 jumper 2 removed, datasheet Table 3):
|
||||
// 272 clocks per row (256 data + 16 blanking), 261 rows per frame
|
||||
// (256 visible + 5 blanking) -> 5e6 / (272 * 261) ~= 70.4 Hz.
|
||||
//
|
||||
// Drawing goes into a back buffer; show() copies it to the displayed buffer at
|
||||
// the start of the next frame, so a redraw never shows half-drawn.
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
struct lldesc_s; // ESP32 DMA descriptor
|
||||
struct intr_handle_data_t; // esp_intr_alloc handle
|
||||
|
||||
struct EL512Pins {
|
||||
int8_t vclk;
|
||||
int8_t vid;
|
||||
int8_t tvid;
|
||||
int8_t hs;
|
||||
int8_t vs;
|
||||
int8_t enable; // -1 if not connected (panel pulls it high)
|
||||
int8_t lowpwr_n; // -1 if not connected (panel pulls it high)
|
||||
};
|
||||
|
||||
class EL512Panel {
|
||||
public:
|
||||
static constexpr uint16_t WIDTH = 512;
|
||||
static constexpr uint16_t HEIGHT = 256;
|
||||
|
||||
// Timing (two-bit parallel mode)
|
||||
static constexpr uint32_t PIXEL_CLOCK_HZ = 5000000;
|
||||
static constexpr uint16_t DATA_CLOCKS_PER_ROW = WIDTH / 2; // 2 px per VCLK
|
||||
static constexpr uint16_t CLOCKS_PER_ROW = 272;
|
||||
static constexpr uint16_t BLANK_CLOCKS_PER_ROW = CLOCKS_PER_ROW - DATA_CLOCKS_PER_ROW;
|
||||
static constexpr uint16_t LINES_PER_FRAME = 261;
|
||||
static constexpr uint16_t BLANK_LINES = LINES_PER_FRAME - HEIGHT;
|
||||
|
||||
// DMA stream byte bits
|
||||
static constexpr uint8_t BIT_VID = 1 << 0;
|
||||
static constexpr uint8_t BIT_TVID = 1 << 1;
|
||||
static constexpr uint8_t BIT_HS = 1 << 2;
|
||||
static constexpr uint8_t BIT_VS = 1 << 3;
|
||||
|
||||
// Stream bytes per VCLK period (BCK runs at half the I2S byte rate).
|
||||
static constexpr uint8_t BYTES_PER_CLOCK = 2;
|
||||
static constexpr uint16_t ROW_BYTES = CLOCKS_PER_ROW * BYTES_PER_CLOCK;
|
||||
|
||||
// DMA ring: CHUNK_COUNT descriptors of LINES_PER_CHUNK lines each.
|
||||
static constexpr uint16_t LINES_PER_CHUNK = 4;
|
||||
static constexpr uint16_t CHUNK_COUNT = 8;
|
||||
static constexpr uint16_t RING_LINES = LINES_PER_CHUNK * CHUNK_COUNT;
|
||||
static constexpr size_t RING_BYTES = (size_t)RING_LINES * ROW_BYTES;
|
||||
|
||||
static constexpr size_t FRAMEBUFFER_BYTES = (size_t)WIDTH * HEIGHT / 8;
|
||||
|
||||
explicit EL512Panel(const EL512Pins& pins);
|
||||
~EL512Panel();
|
||||
|
||||
// Allocates buffers, sets up control GPIOs and starts the I2S DMA refresh.
|
||||
bool begin();
|
||||
void end();
|
||||
bool running() const { return _running; }
|
||||
|
||||
// Display on/off (ENABLE) and low-power mode (_LOWPWR, active low).
|
||||
void setEnabled(bool on);
|
||||
void setLowPower(bool on);
|
||||
bool enabled() const { return _enabled; }
|
||||
bool lowPower() const { return _lowPower; }
|
||||
|
||||
// 1 bpp drawing buffer, row-major, MSB = leftmost pixel.
|
||||
uint8_t* framebuffer() { return _fb; }
|
||||
void setPixel(int16_t x, int16_t y, bool on);
|
||||
bool getPixel(int16_t x, int16_t y) const;
|
||||
void clear(bool on = false);
|
||||
|
||||
// Hand the drawing buffer to the refresh. Copies it at the start of the next
|
||||
// frame and waits for that (at most one frame, ~14 ms).
|
||||
void show();
|
||||
|
||||
// Encode one scan line (0..LINES_PER_FRAME-1) of the displayed buffer into
|
||||
// ROW_BYTES bytes in DMA order. Lines >= HEIGHT produce blanking.
|
||||
void encodeRow(uint16_t line, uint8_t* out) const;
|
||||
|
||||
// Frame refresh rate implied by the timing constants.
|
||||
static constexpr float refreshHz() {
|
||||
return (float)PIXEL_CLOCK_HZ / ((float)CLOCKS_PER_ROW * LINES_PER_FRAME);
|
||||
}
|
||||
|
||||
private:
|
||||
bool startI2S();
|
||||
void stopI2S();
|
||||
void fillChunk(uint16_t chunk);
|
||||
static void isr(void* arg);
|
||||
|
||||
EL512Pins _pins;
|
||||
uint8_t* _fb = nullptr; // drawing buffer
|
||||
uint8_t* _front = nullptr; // buffer being displayed
|
||||
uint8_t* _ring = nullptr; // RING_BYTES, DMA-capable
|
||||
lldesc_s* _desc = nullptr; // CHUNK_COUNT descriptors, circular
|
||||
intr_handle_data_t* _intr = nullptr;
|
||||
uint16_t _chunkLine[CHUNK_COUNT] = {}; // first line currently in each chunk
|
||||
volatile bool _swapPending = false;
|
||||
bool _running = false;
|
||||
bool _enabled = false;
|
||||
bool _lowPower = false;
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "EL512Core",
|
||||
"version": "0.2.0",
|
||||
"description": "Planar EL512.256-H refresh driver core (ESP-IDF API). Shared by lib/EL512 and the ESPHome component in this folder.",
|
||||
"frameworks": ["arduino", "espidf"],
|
||||
"platforms": "espressif32",
|
||||
"build": {
|
||||
"srcFilter": ["-<*>", "+<el512_panel.cpp>", "+<still_alive.cpp>"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
#include "still_alive.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include "el512_font.h"
|
||||
#include "el512_panel.h"
|
||||
#include "still_alive_data.h"
|
||||
|
||||
namespace el512sa {
|
||||
|
||||
namespace {
|
||||
|
||||
uint8_t fit(int16_t px, int16_t cell, uint8_t cap) {
|
||||
const int n = (px - 2 * TextBox::PAD) / cell;
|
||||
return n > cap ? cap : (uint8_t) n;
|
||||
}
|
||||
|
||||
bool due(uint32_t now, uint32_t at) { return (int32_t) (now - at) >= 0; }
|
||||
|
||||
void drawChar(EL512Panel &panel, int16_t x, int16_t y, char c) {
|
||||
if (c < EL512_FONT_FIRST || c > EL512_FONT_LAST)
|
||||
c = '?';
|
||||
const uint8_t *glyph = &EL512_FONT[(c - EL512_FONT_FIRST) * 5];
|
||||
for (int16_t col = 0; col < 5; col++) {
|
||||
const uint8_t bits = glyph[col];
|
||||
for (int16_t row = 0; row < 8; row++) {
|
||||
if (bits & (1 << row))
|
||||
panel.setPixel(x + col, y + row, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// --- TextBox -----------------------------------------------------------------
|
||||
|
||||
TextBox::TextBox(int16_t x, int16_t y, int16_t w, int16_t h)
|
||||
: x_(x), y_(y), w_(w), h_(h), cols_(fit(w, CHAR_W, MAX_COLS)), rows_(fit(h, CHAR_H, MAX_ROWS)) {
|
||||
clear();
|
||||
}
|
||||
|
||||
void TextBox::clear() {
|
||||
memset(text_, 0, sizeof(text_));
|
||||
row_ = 0;
|
||||
col_ = 0;
|
||||
}
|
||||
|
||||
void TextBox::put(char c) {
|
||||
if (c == '\n') {
|
||||
newLine();
|
||||
return;
|
||||
}
|
||||
if (col_ >= cols_)
|
||||
newLine();
|
||||
text_[row_][col_++] = c;
|
||||
}
|
||||
|
||||
void TextBox::print(const char *s) {
|
||||
while (*s)
|
||||
put(*s++);
|
||||
}
|
||||
|
||||
void TextBox::newLine() {
|
||||
col_ = 0;
|
||||
if (row_ + 1 < rows_) {
|
||||
row_++;
|
||||
return;
|
||||
}
|
||||
memmove(text_[0], text_[1], (size_t) (rows_ - 1) * sizeof(text_[0]));
|
||||
memset(text_[rows_ - 1], 0, sizeof(text_[0]));
|
||||
}
|
||||
|
||||
void TextBox::draw(EL512Panel &panel, bool cursor, bool frame) const {
|
||||
if (frame) {
|
||||
// Dashed outline, 4 px on / 2 px off.
|
||||
for (int16_t i = 0; i < w_; i++) {
|
||||
if (i % 6 < 4) {
|
||||
panel.setPixel(x_ + i, y_, true);
|
||||
panel.setPixel(x_ + i, y_ + h_ - 1, true);
|
||||
}
|
||||
}
|
||||
for (int16_t i = 0; i < h_; i++) {
|
||||
if (i % 6 < 4) {
|
||||
panel.setPixel(x_, y_ + i, true);
|
||||
panel.setPixel(x_ + w_ - 1, y_ + i, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (uint8_t r = 0; r < rows_; r++) {
|
||||
for (uint8_t c = 0; c < cols_ && text_[r][c]; c++) {
|
||||
if (text_[r][c] != ' ')
|
||||
drawChar(panel, x_ + PAD + c * CHAR_W, y_ + PAD + r * CHAR_H, text_[r][c]);
|
||||
}
|
||||
}
|
||||
if (cursor) {
|
||||
for (int16_t dy = 6; dy < 8; dy++)
|
||||
for (int16_t dx = 0; dx < 5; dx++)
|
||||
panel.setPixel(x_ + PAD + col_ * CHAR_W + dx, y_ + PAD + row_ * CHAR_H + dy, true);
|
||||
}
|
||||
}
|
||||
|
||||
// --- StillAlivePlayer ----------------------------------------------------------
|
||||
|
||||
// 41 x 31 lyrics console | 41 x 9 credits over a 41 x 20 picture.
|
||||
StillAlivePlayer::StillAlivePlayer()
|
||||
: lyrics_(0, 0, 254, 256), credits_(256, 0, 256, 86), art_(256, 88, 256, 168) {}
|
||||
|
||||
uint32_t StillAlivePlayer::songStartMs() { return START_DELAY_MS + SONG_START_MS; }
|
||||
|
||||
void StillAlivePlayer::start(uint32_t nowMs) {
|
||||
lyrics_.clear();
|
||||
credits_.clear();
|
||||
showArt(ART_NONE);
|
||||
showStart_ = nowMs + START_DELAY_MS;
|
||||
step_ = 0;
|
||||
charIdx_ = 0;
|
||||
stepStarted_ = false;
|
||||
done_ = false;
|
||||
doneAt_ = 0;
|
||||
creditIdx_ = 0;
|
||||
creditChar_ = 0;
|
||||
creditLineOpen_ = false;
|
||||
creditsDone_ = false;
|
||||
creditLineAt_ = CREDITS_START_MS;
|
||||
// Credits are typed on the bottom row and scroll up, as in the game.
|
||||
for (uint8_t r = 1; r < credits_.rows(); r++)
|
||||
credits_.put('\n');
|
||||
}
|
||||
|
||||
bool StillAlivePlayer::finished(uint32_t nowMs) const { return done_ && due(nowMs, doneAt_ + HOLD_MS); }
|
||||
|
||||
void StillAlivePlayer::showArt(int8_t id) {
|
||||
art_.clear();
|
||||
if (id <= ART_NONE || (size_t) id >= ART_COUNT)
|
||||
return;
|
||||
const Art &art = ARTS[id];
|
||||
size_t width = 0;
|
||||
for (uint8_t i = 0; i < art.count; i++) {
|
||||
const size_t len = strlen(art.lines[i]);
|
||||
if (len > width)
|
||||
width = len;
|
||||
}
|
||||
const uint8_t top = art.count < art_.rows() ? (art_.rows() - art.count) / 2 : 0;
|
||||
const uint8_t left = width < art_.cols() ? (art_.cols() - width) / 2 : 0;
|
||||
for (uint8_t r = 0; r < top; r++)
|
||||
art_.put('\n');
|
||||
for (uint8_t i = 0; i < art.count && top + i < art_.rows(); i++) {
|
||||
if (i > 0)
|
||||
art_.put('\n');
|
||||
for (uint8_t c = 0; c < left; c++)
|
||||
art_.put(' ');
|
||||
// Truncate instead of wrapping so a wide picture keeps its shape.
|
||||
for (const char *p = art.lines[i]; *p && left + (p - art.lines[i]) < art_.cols(); p++)
|
||||
art_.put(*p);
|
||||
}
|
||||
}
|
||||
|
||||
bool StillAlivePlayer::update(uint32_t nowMs) {
|
||||
bool changed = false;
|
||||
if (due(nowMs, showStart_)) {
|
||||
const uint32_t t = nowMs - showStart_;
|
||||
changed |= updateLyrics(t, nowMs);
|
||||
changed |= updateCredits(t);
|
||||
}
|
||||
const bool blink = (nowMs / CURSOR_BLINK_MS) & 1;
|
||||
const bool creditsBlink = blink && !creditsDone_ && due(nowMs, showStart_ + CREDITS_START_MS);
|
||||
changed |= blink != cursor_ || creditsBlink != creditsCursor_;
|
||||
cursor_ = blink;
|
||||
creditsCursor_ = creditsBlink;
|
||||
return changed;
|
||||
}
|
||||
|
||||
// Type whatever the clock says should be on screen by now.
|
||||
bool StillAlivePlayer::updateLyrics(uint32_t t, uint32_t nowMs) {
|
||||
bool changed = false;
|
||||
while (step_ < LYRIC_COUNT && t >= LYRICS[step_].startMs) {
|
||||
const LyricStep &s = LYRICS[step_];
|
||||
if (!stepStarted_) {
|
||||
stepStarted_ = true;
|
||||
if (s.flags & STEP_CLEAR)
|
||||
lyrics_.clear();
|
||||
if (s.art != ART_KEEP)
|
||||
showArt(s.art);
|
||||
changed = true;
|
||||
}
|
||||
const size_t len = strlen(s.text);
|
||||
const uint32_t elapsed = t - s.startMs;
|
||||
const size_t want = (s.durMs == 0 || elapsed >= s.durMs) ? len : len * elapsed / s.durMs;
|
||||
while (charIdx_ < want) {
|
||||
lyrics_.put(s.text[charIdx_++]);
|
||||
changed = true;
|
||||
}
|
||||
if (charIdx_ < len)
|
||||
break; // still typing this step
|
||||
if (s.flags & STEP_NEWLINE)
|
||||
lyrics_.put('\n');
|
||||
step_++;
|
||||
charIdx_ = 0;
|
||||
stepStarted_ = false;
|
||||
changed = true;
|
||||
if (step_ == LYRIC_COUNT) {
|
||||
done_ = true;
|
||||
doneAt_ = nowMs;
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
// Each credits line takes CREDIT_CHAR_US per character (an empty line counts as
|
||||
// one), typed evenly; then the next line starts on a fresh bottom row.
|
||||
bool StillAlivePlayer::updateCredits(uint32_t t) {
|
||||
if (creditsDone_ || t < CREDITS_START_MS)
|
||||
return false;
|
||||
bool changed = false;
|
||||
while (true) {
|
||||
const char *line = CREDITS[creditIdx_];
|
||||
const size_t len = strlen(line);
|
||||
if (!creditLineOpen_) {
|
||||
creditLineOpen_ = true;
|
||||
credits_.put('\n');
|
||||
changed = true;
|
||||
}
|
||||
const uint32_t dur = (uint32_t) ((uint64_t) CREDIT_CHAR_US * (len ? len : 1) / 1000);
|
||||
const uint32_t elapsed = t - creditLineAt_;
|
||||
const size_t want = elapsed >= dur ? len : len * elapsed / dur;
|
||||
while (creditChar_ < want) {
|
||||
credits_.put(line[creditChar_++]);
|
||||
changed = true;
|
||||
}
|
||||
if (elapsed < dur)
|
||||
break;
|
||||
creditLineAt_ += dur;
|
||||
creditChar_ = 0;
|
||||
creditLineOpen_ = false;
|
||||
if (++creditIdx_ == CREDIT_COUNT) {
|
||||
creditIdx_ = 0;
|
||||
if (!CREDITS_LOOP) {
|
||||
creditsDone_ = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
void StillAlivePlayer::draw(EL512Panel &panel) const {
|
||||
lyrics_.draw(panel, cursor_);
|
||||
credits_.draw(panel, creditsCursor_);
|
||||
art_.draw(panel, false, false); // no frame around the picture, as in the game
|
||||
}
|
||||
|
||||
} // namespace el512sa
|
||||
@@ -0,0 +1,110 @@
|
||||
// Portal-style end-credits show for the EL512.256-H: timed song text typed into
|
||||
// a console on the left, credits typed top right, ASCII art bottom right.
|
||||
//
|
||||
// Framework-agnostic: draws straight into an EL512Panel with the built-in 5x7
|
||||
// font, so the PlatformIO demo (src/still_alive) and the ESPHome component use
|
||||
// the same code. The content is in still_alive_data.h, generated by
|
||||
// tools/import_still_alive.py.
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
class EL512Panel;
|
||||
|
||||
namespace el512sa {
|
||||
|
||||
enum : uint8_t {
|
||||
STEP_NEWLINE = 1 << 0, // start a new line after the text is typed
|
||||
STEP_CLEAR = 1 << 1, // clear the console before typing
|
||||
};
|
||||
|
||||
// One timed piece of console text. Steps are sorted by startMs.
|
||||
struct LyricStep {
|
||||
uint32_t startMs; // from the start of the show
|
||||
uint16_t durMs; // the text is typed out evenly over this time
|
||||
uint8_t flags;
|
||||
int8_t art; // picture to show (index into ARTS), or ART_KEEP
|
||||
const char *text;
|
||||
};
|
||||
|
||||
struct Art {
|
||||
const char *const *lines;
|
||||
uint8_t count;
|
||||
};
|
||||
|
||||
static constexpr int8_t ART_KEEP = -1; // leave the current picture
|
||||
static constexpr int8_t ART_NONE = 0; // ARTS[0] is always the empty picture
|
||||
|
||||
// Fixed-size character console in the 5x7 font (6x8 cells), optionally inside a
|
||||
// dashed frame. Text wraps at the right edge and scrolls up past the bottom row.
|
||||
class TextBox {
|
||||
public:
|
||||
static constexpr uint8_t MAX_COLS = 48;
|
||||
static constexpr uint8_t MAX_ROWS = 32;
|
||||
static constexpr int16_t PAD = 4;
|
||||
static constexpr int16_t CHAR_W = 6;
|
||||
static constexpr int16_t CHAR_H = 8;
|
||||
|
||||
TextBox(int16_t x, int16_t y, int16_t w, int16_t h);
|
||||
|
||||
uint8_t cols() const { return cols_; }
|
||||
uint8_t rows() const { return rows_; }
|
||||
|
||||
void clear();
|
||||
void put(char c);
|
||||
void print(const char *s);
|
||||
void draw(EL512Panel &panel, bool cursor, bool frame = true) const;
|
||||
|
||||
private:
|
||||
void newLine();
|
||||
|
||||
int16_t x_, y_, w_, h_;
|
||||
uint8_t cols_, rows_;
|
||||
uint8_t row_ = 0, col_ = 0;
|
||||
char text_[MAX_ROWS][MAX_COLS + 1];
|
||||
};
|
||||
|
||||
class StillAlivePlayer {
|
||||
public:
|
||||
static constexpr uint32_t START_DELAY_MS = 1000; // after start()
|
||||
static constexpr uint32_t CURSOR_BLINK_MS = 300;
|
||||
static constexpr uint32_t HOLD_MS = 10000; // after the last step, before finished()
|
||||
|
||||
StillAlivePlayer();
|
||||
|
||||
// Restart the show; it begins START_DELAY_MS after nowMs.
|
||||
void start(uint32_t nowMs);
|
||||
// Advance to nowMs; true if the screen changed (text, picture or cursor).
|
||||
bool update(uint32_t nowMs);
|
||||
// Draw the whole show; the caller clears the panel first and shows it after.
|
||||
void draw(EL512Panel &panel) const;
|
||||
// True once the last step has been typed and HOLD_MS has passed.
|
||||
bool finished(uint32_t nowMs) const;
|
||||
// When to start the music, relative to start().
|
||||
static uint32_t songStartMs();
|
||||
|
||||
private:
|
||||
bool updateLyrics(uint32_t t, uint32_t nowMs);
|
||||
bool updateCredits(uint32_t t);
|
||||
void showArt(int8_t id);
|
||||
|
||||
TextBox lyrics_;
|
||||
TextBox credits_;
|
||||
TextBox art_;
|
||||
uint32_t showStart_ = 0;
|
||||
size_t step_ = 0;
|
||||
size_t charIdx_ = 0;
|
||||
bool stepStarted_ = false;
|
||||
bool done_ = false;
|
||||
uint32_t doneAt_ = 0;
|
||||
size_t creditIdx_ = 0;
|
||||
size_t creditChar_ = 0;
|
||||
bool creditLineOpen_ = false;
|
||||
bool creditsDone_ = false;
|
||||
uint32_t creditLineAt_ = 0;
|
||||
bool cursor_ = false;
|
||||
bool creditsCursor_ = false;
|
||||
};
|
||||
|
||||
} // namespace el512sa
|
||||
@@ -0,0 +1,766 @@
|
||||
// Song text, timing, ASCII art and credits from Portal's ending sequence,
|
||||
// imported from still-alive-web by SD SkyKlouD:
|
||||
// https://github.com/SDSkyKlouD/still-alive-web
|
||||
//
|
||||
// Disclaimer: "Still Alive" was written by Jonathan Coulton for Portal (2007).
|
||||
// Portal, its credits and ASCII art are (c) Valve Corporation, see
|
||||
// https://store.steampowered.com/legal/. Included only for personal,
|
||||
// non-commercial use in this hobby project; all rights remain with their
|
||||
// owners. This project is not affiliated with or endorsed by Valve.
|
||||
//
|
||||
// Generated by tools/import_still_alive.py - do not edit by hand.
|
||||
#pragma once
|
||||
|
||||
#include "still_alive.h"
|
||||
|
||||
namespace el512sa {
|
||||
|
||||
static const char* const ART_APERTURE_LINES[] = {
|
||||
" .,-:;//;:=,",
|
||||
" . :H@@@MM@M#H/.,+%;,",
|
||||
" ,/X+ +M@@M@MM%=,-%HMMM@X/,",
|
||||
" -+@MM; $M@@MH+-,;XMMMM@MMMM@+-",
|
||||
" ;@M@@M- XM@X;. -+XXXXXHHH@M@M#@/.",
|
||||
" ,%MM@@MH ,@%= .---=-=:=,.",
|
||||
" =@#@@@MX ., -%HX$$%%%+;",
|
||||
" =-./@M@M$ .;@MMMM@MM:",
|
||||
" X@/ -$MM/ .+MM@@@M$",
|
||||
",@M@H: :@: . =X#@@@@-",
|
||||
",@@@MMX, . /H- ;@M@M=",
|
||||
".H@@@@M@+, %MM+..%#$.",
|
||||
" /MMMM@MMH/. XM@MH; =;",
|
||||
" /%+%$XHH@$= , .H@@@@MX,",
|
||||
" .=--------. -%H.,@@@@@MX,",
|
||||
" .%MM@@@HHHXX$$$%+- .:$MMX =M@@MM%.",
|
||||
" =XMMM@MM@MM#H;,-+HMM@M+ /MMMX=",
|
||||
" =%@M@M#@$-.=$@MM@@@M; %M%=",
|
||||
" ,:+$+-,/H#MMMMMMM@= =,",
|
||||
" =++%%%%+/:-.",
|
||||
};
|
||||
|
||||
static const char* const ART_CAKE_LINES[] = {
|
||||
" ,:/+/-",
|
||||
" /M/ .,-=;//;-",
|
||||
" .:/= ;MH/, ,=/+%$XH@MM#@:",
|
||||
" -$##@+$###@H@MMM#######H:. -/H#",
|
||||
" .,H@H@ X######@ -H#####@+- -+H###@X",
|
||||
" .,@##H; +XM##M/, =%@###@X;-",
|
||||
"X%- :M##########$. .:%M###@%:",
|
||||
"M##H, +H@@@$/-. ,;$M###@%, -",
|
||||
"M####M=,,---,.-%%H####M$: ,+@##",
|
||||
"@##################@/. :%H##@$-",
|
||||
"M###############H, ;HM##M$=",
|
||||
"#################. .=$M##M$=",
|
||||
"################H..;XM##M$= .:+",
|
||||
"M###################@%= =+@MH%",
|
||||
"@################M/. =+H#X%=",
|
||||
"=+M##############M, -/X#X+;.",
|
||||
" .;XM##########H= ,/X#H+:,",
|
||||
" .=+HM######M+/+HM@+=.",
|
||||
" ,:/%XM####H/.",
|
||||
" ,.:=-.",
|
||||
};
|
||||
|
||||
static const char* const ART_EXPLOSION_LINES[] = {
|
||||
" .+",
|
||||
" /M;",
|
||||
" H#@: ;,",
|
||||
" -###H- -@/",
|
||||
" %####$. -; .%#X",
|
||||
" M#####+;#H :M#M.",
|
||||
".. .+/;%#########X###-",
|
||||
" -/%H%+;-, +##############/",
|
||||
" .:$M###MH$%+############X ,--=;-",
|
||||
" -/H#####################H+=.",
|
||||
" .+#################X.",
|
||||
" =%M####################H;.",
|
||||
" /@###############+;;/%%;,",
|
||||
" -%###################$.",
|
||||
" ;H######################M=",
|
||||
" ,%#####MH$%;+#####M###-/@####%",
|
||||
" :$H%+;=- -####X.,H# -+M##@-",
|
||||
" . ,###; ; =$##+",
|
||||
" .#H, :XH,",
|
||||
" + .;-",
|
||||
};
|
||||
|
||||
static const char* const ART_RADIO_LINES[] = {
|
||||
" ;=",
|
||||
" /=",
|
||||
" ;=",
|
||||
" /=",
|
||||
" ;=",
|
||||
" /=",
|
||||
" ;=",
|
||||
" /=",
|
||||
" ,--==-:$;",
|
||||
" ,/$@#######@X+-",
|
||||
" ./@###############X=",
|
||||
" /M#####X+/;;;;+H#####$.",
|
||||
" %####M/;+H@XX@@%;;@####@,",
|
||||
" +####H=+##$,--,=M#X-%####@.",
|
||||
" -####X,X@HHXH##MXHXXH-+####$",
|
||||
" X###@.X/$M$:####$=@X/X,X####-",
|
||||
".####:+$:##@:####$:##H/X=####%",
|
||||
"-%%$%,+==%$+-$+:$;-$$%-+,/$%%+",
|
||||
"-/+%%X$XX$$$$$$$%$$$%$X$X$%+/-",
|
||||
};
|
||||
|
||||
static const char* const ART_ATOM_LINES[] = {
|
||||
" =/;;/-",
|
||||
" +: //",
|
||||
" /; /;",
|
||||
" -X H.",
|
||||
".//;;;:;;-, X= :+ .-;:=;:;%;.",
|
||||
"M- ,=;;;#:, ,:#;;:=, ,@",
|
||||
":% :%.=/++++/=.$= %=",
|
||||
" ,%; %/:+/;,,/++:+/ ;+.",
|
||||
" ,+/. ,;@+, ,%H;, ,/+,",
|
||||
" ;+;;/= @. .H##X -X :///+;",
|
||||
" ;+=;;;.@, .XM@$. =X.//;=%/.",
|
||||
" ,;: :@%= =$H: .+%-",
|
||||
" ,%= %;-///==///-// =%,",
|
||||
";+ :%-;;;:;;;;-X- +:",
|
||||
"@- .-;;;;M- =M/;;;-. -X",
|
||||
" :;;::;;-. %- :+ ,-;;-;:==",
|
||||
" ,X H.",
|
||||
" ;/ %=",
|
||||
" // +;",
|
||||
" ,////,",
|
||||
};
|
||||
|
||||
static const char* const ART_BIOHAZARD_LINES[] = {
|
||||
" =+$HM####@H%;,",
|
||||
" /H###############M$,",
|
||||
" ,@################+",
|
||||
" .H##############+",
|
||||
" X############/",
|
||||
" $##########/",
|
||||
" %########/",
|
||||
" /X/;;+X/",
|
||||
"",
|
||||
" -XHHX-",
|
||||
" ,######,",
|
||||
"#############X .M####M. X#############",
|
||||
"##############- -//- -##############",
|
||||
"X##############%, ,+##############X",
|
||||
"-##############X X##############-",
|
||||
" %############% %############%",
|
||||
" %##########; ;##########%",
|
||||
" ;#######M= =M#######;",
|
||||
" .+M###@, ,@###M+.",
|
||||
" :XH. .HX:",
|
||||
};
|
||||
|
||||
static const char* const ART_BLACKMESA_LINES[] = {
|
||||
" .-;+$XHHHHHHX$+;-.",
|
||||
" ,;X@@X%/;=----=:/%X@@X/,",
|
||||
" =$@@%=. .=+H@X:",
|
||||
" -XMX: =XMX=",
|
||||
" /@@: =H@+",
|
||||
" %@X, .$@$",
|
||||
" +@X. $@%",
|
||||
"-@@, .@@=",
|
||||
"%@% +@$",
|
||||
"H@: :@H",
|
||||
"H@: :HHHHHHHHHHHHHHHHHHX, =@H",
|
||||
"%@% ;@M@@@@@@@@@@@@@@@@@H- +@$",
|
||||
"=@@, :@@@@@@@@@@@@@@@@@@@@@= .@@:",
|
||||
" +@X :@@@@@@@@@@@@@@@M@@@@@@:%@%",
|
||||
" $@$, ;@@@@@@@@@@@@@@@@@M@@@@@@$.",
|
||||
" +@@HHHHHHH@@@@@@@@@@@@@@@@@@@@@@@+",
|
||||
" =X@@@@@@@@@@@@@@@@@@@@@@@@@@@@X=",
|
||||
" :$@@@@@@@@@@@@@@@@@@@M@@@@$:",
|
||||
" ,;$@@@@@@@@@@@@@@@@@@X/-",
|
||||
" .-;+$XXHHHHHX$+;-.",
|
||||
};
|
||||
|
||||
static const char* const ART_BROKENHEART_LINES[] = {
|
||||
" .,---.",
|
||||
" ,/XM#MMMX;,",
|
||||
" -%##########M%,",
|
||||
" -@######% $###@=",
|
||||
" .,--, -H#######$ $###M:",
|
||||
" ,;$M###MMX; .;##########$;HM###X=",
|
||||
" ,/@##########H= ;################+",
|
||||
"-+#############M/, %##############+",
|
||||
"%M###############= /##############:",
|
||||
"H################ .M#############;.",
|
||||
"@###############M ,@###########M:.",
|
||||
"X################, -$=X#######@:",
|
||||
"/@##################%- +######$-",
|
||||
".;##################X .X#####+,",
|
||||
" .;H################/ -X####+.",
|
||||
" ,;X##############, .MM/",
|
||||
" ,:+$H@M#######M#$- .$$=",
|
||||
" .,-=;+$@###X: ;/=.",
|
||||
" .,/X$; .::,",
|
||||
" ., ..",
|
||||
};
|
||||
|
||||
static const char* const ART_CHECK_LINES[] = {
|
||||
" :X-",
|
||||
" :X###",
|
||||
" ;@####@",
|
||||
" ;M######X",
|
||||
" -@########$",
|
||||
" .$##########@",
|
||||
" =M############-",
|
||||
" +##############$",
|
||||
" .H############$=.",
|
||||
" ,/: ,M##########M;.",
|
||||
" -+@###; =##########M;",
|
||||
" =%M#######; :#########M/",
|
||||
"-$M###########; :#########/",
|
||||
" ,;X###########; =########$.",
|
||||
" ;H#########+#######M=",
|
||||
" ,+##############+",
|
||||
" /M#########@-",
|
||||
" ;M######%",
|
||||
" +####:",
|
||||
" ,$M-",
|
||||
};
|
||||
|
||||
static const char* const ART_COMPANIONCUBE_LINES[] = {
|
||||
"+@##########M/ :@#########@/",
|
||||
"##############$;H#######@;+#############",
|
||||
"###############M########################",
|
||||
"##############X,-/++/+%+/,%#############",
|
||||
"############M$: -X############",
|
||||
"##########H;. ,--. =X##########",
|
||||
":X######M; -$H@M##MH%: :H#######@",
|
||||
" =%#M+=, ,+@#######M###H: -=/M#%",
|
||||
" %M##@+ .X##$, ./+- ./###; +M##%",
|
||||
" %####M. /###= @##M. X###%",
|
||||
" %####M. ;M##H:. =$###X. $###%",
|
||||
" %####@. /####M$-./@#####: %###%",
|
||||
" %H#M/, /H###########@: ./M#%",
|
||||
" ;$H##@@H: .;$HM#MMMH$;, ./H@M##M$=",
|
||||
"X#########%. ..,,. .;@#########",
|
||||
"###########H+:. ./@###########",
|
||||
"##############/ ./%%%%+/.-M#############",
|
||||
"##############H$@#######@@##############",
|
||||
"##############X%########M$M#############",
|
||||
"+M##########H: .$##########X=",
|
||||
};
|
||||
|
||||
static const char* const ART_FIRE_LINES[] = {
|
||||
" -$-",
|
||||
" .H##H,",
|
||||
" +######+",
|
||||
" .+#########H.",
|
||||
" -$############@.",
|
||||
" =H###############@ -X:",
|
||||
" .$##################: @#@-",
|
||||
" ,; .M###################; H###;",
|
||||
" ;@#: @###################@ ,#####:",
|
||||
" -M###. M#################@. ;######H",
|
||||
" M####- +###############$ =@#######X",
|
||||
" H####$ -M###########+ :#########M,",
|
||||
" /####X- =########% :M########@/.",
|
||||
" ,;%H@X; .$###X :##MM@%+;:-",
|
||||
" ..",
|
||||
" -/;:-,. ,,-==+M########H",
|
||||
" -##################@HX%%+%%$%%%+:,,",
|
||||
" .-/H%%%+%%$H@###############M@+=:/+:",
|
||||
"/XHX%:#####MH%= ,---:;;;;/%%XHM,:###$",
|
||||
};
|
||||
|
||||
static const char* const ART_GLADOS_LINES[] = {
|
||||
" #+ @ # # M#@",
|
||||
" . .X X.%##@;# # +@#######X. @#%",
|
||||
" ,==. ,######M+ -#####%M####M- #",
|
||||
" :H##M%:=##+ .M##M,;#####/+#######% ,M#",
|
||||
" .M########= =@#@.=#####M=M#######= X#",
|
||||
" :@@MMM##M. -##M.,#######M#######. = M",
|
||||
" @##..###:. .H####. @@ X,",
|
||||
" ############: ###,/####; /##= @#. M",
|
||||
" ,M## ;##,@#M;/M#M @# X#% X#",
|
||||
".%= ######M## ##.M#: ./#M ,M #M ,#$",
|
||||
"##/ $## #+;#: #### ;#/ M M- @# :",
|
||||
"#+ #M@MM###M-;M #:$#-##$H# .#X @ + $#. #",
|
||||
" ######/.: #%=# M#:MM./#.-# @#: H#",
|
||||
"+,.= @###: /@ %#,@ ##@X #,-#@.##% .@#",
|
||||
"#####+;/##/ @## @#,+ /#M . X,",
|
||||
" ;###M#@ M###H .#M- ,##M ;@@; ###\"",
|
||||
" .M#M##H ;####X ,@#######M/ -M###$ -H",
|
||||
" .M###% X####H .@@MM@; ;@#M@",
|
||||
" H#M /@####/ ,++. / ==-,",
|
||||
" ,=/:, .+X@MMH@#H #####$=",
|
||||
};
|
||||
|
||||
static const char* const ART_LIGHTBULB_LINES[] = {
|
||||
" ,=;%$%%$X%%%%;/%%%%;=,",
|
||||
" ,/$$+:- -:+$$/,",
|
||||
" :X$= =$X:",
|
||||
" ;M%. .%M;",
|
||||
"+#/ /#+",
|
||||
"## M#",
|
||||
"H#, =;+/;, ,#X",
|
||||
".HM- :@X+%H: .%M%- .M#. -M@.",
|
||||
" /#%. @#- ,H@--MH, .;@$- .%#+",
|
||||
" .$M; .+@X;, MM#@:/$X;. ;M$,",
|
||||
" =@H, ,:+%H#M%;- ,H@=",
|
||||
" .$#; -#H =#$",
|
||||
" %#; #M ;#%",
|
||||
" H#- ## -#H",
|
||||
" ;#+ ## +#;",
|
||||
" ;H+;;;;;;HH;;;;;;+H/",
|
||||
" =H#@HHHHHHHHHH@#H=",
|
||||
" =@#H%%%%%%%$HH@#@=",
|
||||
" =@#X%%%%%%%$M###@=",
|
||||
" =+%XHHX%+=",
|
||||
};
|
||||
|
||||
static const char* const ART_MECHARM_LINES[] = {
|
||||
" X MM X",
|
||||
" X MM X",
|
||||
" X MM X",
|
||||
" X MM X",
|
||||
" + HX +",
|
||||
" ,=$$XX%/-",
|
||||
" =X#########@%-",
|
||||
" ;##############=",
|
||||
"-###############M,",
|
||||
";##@@@######M@###=",
|
||||
".+:;+:=H##$=:/:;H.",
|
||||
"- +###- ## :###,,;",
|
||||
"+@:/%;-H##H==/::H;",
|
||||
" /#@/-=+$$%::+H#$",
|
||||
" $#%-, ,.:##-",
|
||||
"-@/ =X%.",
|
||||
"%H= -$;",
|
||||
" =HH, .%M;",
|
||||
" /MM/ :@M/.",
|
||||
" .:XX, -$H:.",
|
||||
};
|
||||
|
||||
static const char* const ART_PERSONALITYSPHERE_LINES[] = {
|
||||
" .-+$H###MM@MMMMM##@$+-,. ....",
|
||||
"-@$+%$+%HX+--.. . . .,:X$/+/++$#:",
|
||||
"-#MXH$= $HXH#:",
|
||||
" .--,:#+ ,+$HMX =@@X%, . .X#:,,,",
|
||||
" =#@$H :####H =####;,M%$#X",
|
||||
" X###$ $####X =####H %###X",
|
||||
" ;###X /###@$: ,+HM##H.+###;",
|
||||
" :###;,X##%=;%H@H$;-;M#@-;###/",
|
||||
" ,M##;.@##;-H#######M=.M##-:###-",
|
||||
" ;##M ;##X @###H-=@###.;##X H##;",
|
||||
" ;##M./##X.@###H:/M###-=##X X##;",
|
||||
" -###;,M##:,@########+-H##; @##-",
|
||||
" %##M==@##%==%HMH%::/M##+.X##+",
|
||||
" %###/./###X+: -+$M##M=,X##+",
|
||||
" X###X X####H +#####% @##H",
|
||||
" :###H %####H +#####; X##;",
|
||||
" /#$. -HM##H /###@+. +#$. .",
|
||||
"/HX%$X: .,-, .-,. =XX$H@-",
|
||||
"/#H+/+%+/+;=. .=/%;;/;;+#+",
|
||||
" .. .,-:XM#MM@@@@@@H@@M#@+=,. ,,",
|
||||
};
|
||||
|
||||
static const Art ARTS[] = {
|
||||
{nullptr, 0},
|
||||
{ART_APERTURE_LINES, 20}, // 1
|
||||
{ART_CAKE_LINES, 20}, // 2
|
||||
{ART_EXPLOSION_LINES, 20}, // 3
|
||||
{ART_RADIO_LINES, 19}, // 4
|
||||
{ART_ATOM_LINES, 20}, // 5
|
||||
{ART_BIOHAZARD_LINES, 20}, // 6
|
||||
{ART_BLACKMESA_LINES, 20}, // 7
|
||||
{ART_BROKENHEART_LINES, 20}, // 8
|
||||
{ART_CHECK_LINES, 20}, // 9
|
||||
{ART_COMPANIONCUBE_LINES, 20}, // 10
|
||||
{ART_FIRE_LINES, 19}, // 11
|
||||
{ART_GLADOS_LINES, 20}, // 12
|
||||
{ART_LIGHTBULB_LINES, 20}, // 13
|
||||
{ART_MECHARM_LINES, 20}, // 14
|
||||
{ART_PERSONALITYSPHERE_LINES, 20}, // 15
|
||||
};
|
||||
static constexpr size_t ART_COUNT = sizeof(ARTS) / sizeof(ARTS[0]);
|
||||
|
||||
static const LyricStep LYRICS[] = {
|
||||
{0, 0, STEP_CLEAR, ART_KEEP, ""},
|
||||
{0, 2000, STEP_NEWLINE, ART_KEEP, "Forms FORM-29827281-12:"},
|
||||
{2000, 2000, STEP_NEWLINE, ART_KEEP, "Test Assessment Report"},
|
||||
{4010, 0, STEP_NEWLINE, ART_KEEP, ""},
|
||||
{4020, 0, STEP_NEWLINE, ART_KEEP, ""},
|
||||
{6870, 2050, STEP_NEWLINE, ART_KEEP, "This was a triumph."},
|
||||
{10820, 2100, STEP_NEWLINE, ART_KEEP, "I'm making a note here:"},
|
||||
{12930, 1800, STEP_NEWLINE, ART_KEEP, "HUGE SUCCESS."},
|
||||
{15730, 2360, STEP_NEWLINE, ART_KEEP, "It's hard to overstate"},
|
||||
{18130, 2710, STEP_NEWLINE, ART_KEEP, "my satisfaction."},
|
||||
{22970, 1760, STEP_NEWLINE, 1, "Aperture Science"},
|
||||
{26830, 1570, STEP_NEWLINE, ART_KEEP, "We do what we must"},
|
||||
{28600, 750, 0, ART_KEEP, "because "},
|
||||
{29550, 710, STEP_NEWLINE, ART_KEEP, "we can."},
|
||||
{31930, 1550, 0, ART_KEEP, "For the good "},
|
||||
{33480, 1530, STEP_NEWLINE, ART_KEEP, "of all of us."},
|
||||
{35289, 1741, STEP_NEWLINE, 6, "Except the ones who are dead."},
|
||||
{37030, 400, STEP_NEWLINE, ART_KEEP, ""},
|
||||
{37430, 1858, STEP_NEWLINE, 1, "But there's no sense crying"},
|
||||
{39298, 1782, STEP_NEWLINE, ART_KEEP, "over every mistake."},
|
||||
{41280, 1985, STEP_NEWLINE, ART_KEEP, "You just keep on trying"},
|
||||
{43275, 1366, 0, ART_KEEP, "till you run out of "},
|
||||
{44641, 400, STEP_NEWLINE, ART_KEEP, "cake."},
|
||||
{45227, 1970, STEP_NEWLINE, 5, "And the Science gets done."},
|
||||
{47197, 1970, STEP_NEWLINE, ART_KEEP, "And you make a neat gun."},
|
||||
{49187, 1520, STEP_NEWLINE, 1, "For the people who are"},
|
||||
{50727, 1500, STEP_NEWLINE, ART_KEEP, "still alive."},
|
||||
{53827, 0, STEP_CLEAR, ART_KEEP, ""},
|
||||
{53827, 550, STEP_NEWLINE, ART_KEEP, "Forms FORM-55551-5:"},
|
||||
{54377, 1100, STEP_NEWLINE, ART_KEEP, "Personnel File Addendum:"},
|
||||
{55477, 0, STEP_NEWLINE, ART_KEEP, ""},
|
||||
{58487, 0, STEP_NEWLINE, ART_KEEP, ""},
|
||||
{58547, 1917, STEP_NEWLINE, ART_KEEP, "I'm not even angry."},
|
||||
{62832, 697, 0, ART_KEEP, "I'm being "},
|
||||
{63529, 650, 0, ART_KEEP, "so "},
|
||||
{64179, 906, 0, ART_KEEP, "sincere "},
|
||||
{65085, 1115, STEP_NEWLINE, ART_KEEP, "right now."},
|
||||
{67825, 650, 0, ART_KEEP, "Even "},
|
||||
{68475, 1347, 0, ART_KEEP, "though you "},
|
||||
{69822, 488, 0, 8, "broke "},
|
||||
{70310, 998, STEP_NEWLINE, ART_KEEP, "my heart."},
|
||||
{71308, 325, 0, ART_KEEP, "And "},
|
||||
{71633, 488, 0, ART_KEEP, "killed "},
|
||||
{72121, 557, STEP_NEWLINE, ART_KEEP, "me."},
|
||||
{74678, 1649, STEP_NEWLINE, 3, "And tore me to pieces."},
|
||||
{78649, 1440, 0, ART_KEEP, "And threw every piece "},
|
||||
{80091, 580, 0, ART_KEEP, "into "},
|
||||
{81066, 882, STEP_NEWLINE, 11, "a fire."},
|
||||
{83597, 1400, 0, ART_KEEP, "As they burned "},
|
||||
{84997, 1509, STEP_NEWLINE, ART_KEEP, "it hurt because"},
|
||||
{86831, 1625, STEP_NEWLINE, 9, "I was so happy for you!"},
|
||||
{88897, 2067, STEP_NEWLINE, ART_KEEP, "Now these points of data"},
|
||||
{90964, 1950, STEP_NEWLINE, ART_KEEP, "make a beautiful line."},
|
||||
{92914, 2020, STEP_NEWLINE, ART_KEEP, "And we're out of beta."},
|
||||
{94934, 1950, STEP_NEWLINE, ART_KEEP, "We're releasing on time."},
|
||||
{96884, 2113, STEP_NEWLINE, 3, "So I'm GLaD. I got burned."},
|
||||
{98997, 2020, STEP_NEWLINE, 5, "Think of all the things we learned"},
|
||||
{101017, 1556, STEP_NEWLINE, 1, "for the people who are"},
|
||||
{102573, 395, 0, ART_KEEP, "still "},
|
||||
{102968, 1440, STEP_NEWLINE, ART_KEEP, "alive."},
|
||||
{106008, 0, STEP_CLEAR, ART_KEEP, ""},
|
||||
{106008, 450, STEP_NEWLINE, ART_KEEP, "Forms FORM-55551-6:"},
|
||||
{106459, 1310, STEP_NEWLINE, ART_KEEP, "Personnel File Addendum Addendum:"},
|
||||
{107768, 0, STEP_NEWLINE, ART_KEEP, ""},
|
||||
{108428, 2200, STEP_NEWLINE, ART_KEEP, "One last thing:"},
|
||||
{110628, 0, STEP_NEWLINE, ART_KEEP, ""},
|
||||
{110678, 1324, 0, ART_KEEP, "Go ahead and leave "},
|
||||
{112002, 627, STEP_NEWLINE, ART_KEEP, "me."},
|
||||
{114417, 534, 0, ART_KEEP, "I think "},
|
||||
{114951, 1184, 0, ART_KEEP, "I prefer "},
|
||||
{116135, 1022, 0, ART_KEEP, "to stay "},
|
||||
{117157, 232, 0, ART_KEEP, "in"},
|
||||
{117389, 882, STEP_NEWLINE, ART_KEEP, "side."},
|
||||
{119701, 511, 0, ART_KEEP, "May"},
|
||||
{120212, 2972, STEP_NEWLINE, ART_KEEP, "be you'll find someone else"},
|
||||
{123184, 1254, STEP_NEWLINE, ART_KEEP, "to help you."},
|
||||
{126690, 511, 0, ART_KEEP, "Maybe "},
|
||||
{127201, 325, 0, 7, "Black "},
|
||||
{127526, 998, 0, ART_KEEP, "Mesa"},
|
||||
{128524, 2183, STEP_NEWLINE, ART_KEEP, "..."},
|
||||
{130707, 1440, 0, ART_KEEP, "THAT WAS A JOKE. "},
|
||||
{133168, 279, 0, ART_KEEP, "FAT "},
|
||||
{133447, 859, STEP_NEWLINE, ART_KEEP, "CHANCE."},
|
||||
{135699, 1486, 0, ART_KEEP, "Anyway"},
|
||||
{137185, 1765, STEP_NEWLINE, 2, ", this cake is great."},
|
||||
{138950, 1904, STEP_NEWLINE, ART_KEEP, "It's so delicious and moist."},
|
||||
{141272, 1927, STEP_NEWLINE, 12, "Look at me still talking"},
|
||||
{143199, 2090, STEP_NEWLINE, 6, "when there's Science to do."},
|
||||
{145289, 1695, STEP_NEWLINE, 1, "When I look out there,"},
|
||||
{146984, 2183, STEP_NEWLINE, ART_KEEP, "it makes me GLaD I'm not you."},
|
||||
{149167, 2043, STEP_NEWLINE, 5, "I've experiments to run."},
|
||||
{151210, 2020, STEP_NEWLINE, 3, "There is research to be done."},
|
||||
{153230, 1625, STEP_NEWLINE, 1, "On the people who are"},
|
||||
{154855, 348, 0, ART_KEEP, "still "},
|
||||
{155203, 1440, STEP_NEWLINE, ART_KEEP, "alive."},
|
||||
{156843, 0, STEP_CLEAR, ART_KEEP, ""},
|
||||
{156953, 0, STEP_NEWLINE, ART_KEEP, ""},
|
||||
{156963, 0, STEP_NEWLINE, ART_KEEP, ""},
|
||||
{156973, 0, STEP_NEWLINE, ART_KEEP, ""},
|
||||
{156983, 300, 0, ART_NONE, "PS: "},
|
||||
{157283, 1625, STEP_NEWLINE, ART_KEEP, "And believe me I am"},
|
||||
{158908, 1138, STEP_NEWLINE, ART_KEEP, "still alive."},
|
||||
{160846, 198, 0, ART_KEEP, "PPS: "},
|
||||
{161044, 1904, STEP_NEWLINE, ART_KEEP, "I'm doing Science and I'm"},
|
||||
{162948, 1091, STEP_NEWLINE, ART_KEEP, "still alive."},
|
||||
{164839, 175, 0, ART_KEEP, "PPPS: "},
|
||||
{165014, 1904, STEP_NEWLINE, ART_KEEP, "I feel FANTASTIC and I'm"},
|
||||
{166918, 1091, STEP_NEWLINE, ART_KEEP, "still alive."},
|
||||
{168009, 0, STEP_NEWLINE, ART_KEEP, ""},
|
||||
{168109, 1154, STEP_NEWLINE, ART_KEEP, "FINAL THOUGHT: "},
|
||||
{169263, 1579, STEP_NEWLINE, ART_KEEP, "While you're dying I'll be"},
|
||||
{170842, 1254, STEP_NEWLINE, ART_KEEP, "still alive."},
|
||||
{172096, 0, STEP_NEWLINE, ART_KEEP, ""},
|
||||
{172196, 829, STEP_NEWLINE, ART_KEEP, "FINAL THOUGHT PS: "},
|
||||
{173025, 1834, STEP_NEWLINE, ART_KEEP, "And when you're dead I will be"},
|
||||
{174859, 1161, STEP_NEWLINE, ART_KEEP, "still alive."},
|
||||
{176020, 0, STEP_NEWLINE, ART_KEEP, ""},
|
||||
{176479, 0, STEP_NEWLINE, ART_KEEP, ""},
|
||||
{176879, 1184, STEP_NEWLINE, ART_KEEP, "STILL ALIVE"},
|
||||
{179250, 0, STEP_CLEAR, ART_KEEP, ""},
|
||||
{180250, 0, STEP_NEWLINE, ART_KEEP, ""},
|
||||
{181250, 0, STEP_NEWLINE, ART_KEEP, ""},
|
||||
{182250, 0, STEP_NEWLINE, ART_KEEP, ""},
|
||||
{183250, 1500, STEP_NEWLINE, ART_KEEP, " ***"},
|
||||
{184750, 1500, STEP_NEWLINE, ART_KEEP, " Portal? 10th anniversary"},
|
||||
{186250, 1500, STEP_NEWLINE, ART_KEEP, " 'Still Alive' ending scene"},
|
||||
{187750, 1500, STEP_NEWLINE, ART_KEEP, " remade with HTML, JS"},
|
||||
{189250, 2500, STEP_NEWLINE, ART_KEEP, " by SD SkyKlouD"},
|
||||
{191750, 0, STEP_NEWLINE, ART_KEEP, ""},
|
||||
{192250, 600, STEP_NEWLINE, ART_KEEP, " ? 2007- Valve Corporation."},
|
||||
{192850, 900, STEP_NEWLINE, ART_KEEP, " All Rights Reserved."},
|
||||
{193750, 1500, STEP_NEWLINE, ART_KEEP, " Valve, Steam, Portal are trademarks"},
|
||||
{195250, 1500, STEP_NEWLINE, ART_KEEP, " and/or registered trademarks of"},
|
||||
{196750, 1500, STEP_NEWLINE, ART_KEEP, " Valve Corporation. All other trademarks"},
|
||||
{198250, 1500, STEP_NEWLINE, ART_KEEP, " are property of their respective owners."},
|
||||
};
|
||||
static constexpr size_t LYRIC_COUNT = sizeof(LYRICS) / sizeof(LYRICS[0]);
|
||||
|
||||
static const char* const CREDITS[] = {
|
||||
">LIST PERSONNEL",
|
||||
"",
|
||||
"",
|
||||
"Gautam Babbar",
|
||||
"Ted Backman",
|
||||
"Kelly Bailey",
|
||||
"Jeff Ballinger",
|
||||
"Aaron Barber",
|
||||
"Jeep Barnett",
|
||||
"Jeremy Bennett",
|
||||
"Dan Berger",
|
||||
"Yahn Bernier",
|
||||
"Ken Birdwell",
|
||||
"Derrick Birum",
|
||||
"Mike Blaszczak",
|
||||
"Iestyn Bleasdale-Shepherd",
|
||||
"Chris Bokitch",
|
||||
"Steve Bond",
|
||||
"Matt Boone",
|
||||
"Antoine Bourdon",
|
||||
"Jamaal Bradley",
|
||||
"Jason Brashill",
|
||||
"Charlie Brown",
|
||||
"Charlie Burgin",
|
||||
"Andrew Burke",
|
||||
"Augusta Butlin",
|
||||
"Julie Caldwell",
|
||||
"Dario Casali",
|
||||
"Chris Chin",
|
||||
"Jess Cliffe",
|
||||
"Phil Co",
|
||||
"John Cook",
|
||||
"Christen Coomer",
|
||||
"Greg Coomer",
|
||||
"Scott Dalton",
|
||||
"Kerry Davis",
|
||||
"Jason Deakins",
|
||||
"Joe Demers",
|
||||
"Ariel Diaz",
|
||||
"Quintin Doroquez",
|
||||
"Jim Dose",
|
||||
"Chris Douglass",
|
||||
"Laura Dubuk",
|
||||
"Mike Dunkle",
|
||||
"Mike Durand",
|
||||
"Mike Dussault",
|
||||
"Dhabih Eng",
|
||||
"Katie Engel",
|
||||
"Chet Faliszek",
|
||||
"Adrian Finol",
|
||||
"Bill Fletcher",
|
||||
"Moby Francke",
|
||||
"Stephane Gaudette",
|
||||
"Kathy Gehrig",
|
||||
"Vitaliy Genkin",
|
||||
"Paul Graham",
|
||||
"Chris Green",
|
||||
"Chris Grinstead",
|
||||
"John Guthrie",
|
||||
"Aaron Halifax",
|
||||
"Reagan Halifax",
|
||||
"Leslie Hall",
|
||||
"Jeff Hameluck",
|
||||
"Joe Han",
|
||||
"Don Holden",
|
||||
"Jason Holtman",
|
||||
"Gray Horsfield",
|
||||
"Keith Huggins",
|
||||
"Jim Hughes",
|
||||
"Jon Huisingh",
|
||||
"Brian Jacobson",
|
||||
"Lars Jensvold",
|
||||
"Erik Johnson",
|
||||
"Jakob Jungels",
|
||||
"Rich Kaethler",
|
||||
"Steve Kalning",
|
||||
"Aaron Kearly",
|
||||
"Iikka Keranen",
|
||||
"David Kircher",
|
||||
"Eric Kirchmer",
|
||||
"Scott Klintworth",
|
||||
"Alden Kroll",
|
||||
"Marc Laidlaw",
|
||||
"Jeff Lane",
|
||||
"Tim Larkin",
|
||||
"Dan LeFree",
|
||||
"Isabelle LeMay",
|
||||
"Tom Leonard",
|
||||
"Jeff Lind",
|
||||
"Doug Lombardi",
|
||||
"Bianca Loomis",
|
||||
"Richard Lord",
|
||||
"Realm Lovejoy",
|
||||
"Randy Lundeen",
|
||||
"Scott Lynch",
|
||||
"Ido Magal",
|
||||
"Nick Maggiore",
|
||||
"John McCaskey",
|
||||
"Patrick McClard",
|
||||
"Steve McClure",
|
||||
"Hamish McKenzie",
|
||||
"Gary McTaggart",
|
||||
"Jason Mitchell",
|
||||
"Mike Morasky",
|
||||
"John Morello II",
|
||||
"Bryn Moslow",
|
||||
"Arsenio Navarro",
|
||||
"Gabe Newell",
|
||||
"Milton Ngan",
|
||||
"Jake Nicholson",
|
||||
"Martin Otten",
|
||||
"Nick Papineau",
|
||||
"Karen Prell",
|
||||
"Bay Raitt",
|
||||
"Tristan Reidford",
|
||||
"Alfred Reynolds",
|
||||
"Matt Rhoten",
|
||||
"Garret Rickey",
|
||||
"Dave Riller",
|
||||
"Elan Ruskin",
|
||||
"Matthew Russell",
|
||||
"Jason Ruymen",
|
||||
"David Sawyer",
|
||||
"Marc Scaparro",
|
||||
"Wade Schin",
|
||||
"Matthew Scott",
|
||||
"Aaron Seeler",
|
||||
"Jennifer Seeley",
|
||||
"Taylor Sherman",
|
||||
"Eric Smith",
|
||||
"Jeff Sorensen",
|
||||
"David Speyrer",
|
||||
"Jay Stelly",
|
||||
"Jeremy Stone",
|
||||
"Eric Strand",
|
||||
"Kim Swift",
|
||||
"Kelly Thornton",
|
||||
"Eric Twelker",
|
||||
"Carl Uhlman",
|
||||
"Doug Valente",
|
||||
"Bill Van Buren",
|
||||
"Gabe Van Engel",
|
||||
"Alex Vlachos",
|
||||
"Robin Walker",
|
||||
"Joshua Weier",
|
||||
"Andrea Wicklund",
|
||||
"Greg Winkler",
|
||||
"Erik Wolpaw",
|
||||
"Doug Wood",
|
||||
"Matt T. Wood",
|
||||
"Danika Wright",
|
||||
"Matt Wright",
|
||||
"Shawn Zabecki",
|
||||
"Torsten Zabka ",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"'Still Alive' by:",
|
||||
"Jonathan Coulton",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"Voices:",
|
||||
"Ellen McLain - GlaDOS, Turrets",
|
||||
"Mike Patton - THE ANGER SPHERE",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"Voice Casting:",
|
||||
"Shana LandsburgTeri Fiddleman",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"Voice Recording:",
|
||||
"Pure Audio, Seattle, WA",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"Voice recording",
|
||||
"scheduling and logistics:",
|
||||
"Pat Cockburn, Pure Audio",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"Translations:",
|
||||
"SDL",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"Crack Legal Team:",
|
||||
"Liam Lavery",
|
||||
"Karl Quackenbush",
|
||||
"Kristen Boraas",
|
||||
"Kevin Rosenfield",
|
||||
"Alan Bruggeman",
|
||||
"Dennis Tessier",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"Thanks for the use of their face:",
|
||||
"Alesia Glidewell - Chell",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"Special thanks to everyone at:",
|
||||
"Alienware",
|
||||
"ATI",
|
||||
"Dell",
|
||||
"Falcon Northwest",
|
||||
"Havok",
|
||||
"SOFTIMAGE",
|
||||
"and Don Kemmis, SLK Technologies",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"THANK YOU FOR PARTICIPATING",
|
||||
"IN THIS",
|
||||
"ENRICHMENT CENTER ACTIVITY!!",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
};
|
||||
static constexpr size_t CREDIT_COUNT = sizeof(CREDITS) / sizeof(CREDITS[0]);
|
||||
|
||||
static constexpr uint32_t SONG_START_MS = 6750;
|
||||
static constexpr uint32_t CREDITS_START_MS = 9000;
|
||||
static constexpr uint32_t CREDIT_CHAR_US = 68624;
|
||||
static constexpr bool CREDITS_LOOP = false;
|
||||
|
||||
} // namespace el512sa
|
||||
Reference in new issue
Block a user