mirror of
https://github.com/dwhinham/mt32-pi.git
synced 2026-10-26 22:11:05 +00:00
Add support for 64 pixel high SSD1306 displays
Changes to the velocity scaling logic were also made; mt32lcd now normalizes the MT-32 part velocity to a floating point number between 0 and 1. The LCD driver can then translate this into a bar pixel height as required.
This commit is contained in:
1 parent
c6289b9ed1
commit
e7a8a70fe1
10 files changed
+139
-82
No files matched your search
@@ -10,10 +10,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
### Added
|
||||
|
||||
- Ability to invert the display orientation for SSD1306 (new configuration file option).
|
||||
- Support for 64 pixel high SSD1306 OLED displays - many thanks to @ctrl_alt_rees for donating a screen!
|
||||
|
||||
### Fixed
|
||||
|
||||
- SSD1306 text alignment was off by one pixel.
|
||||
- SSD1306 part level meters' top pixels would flicker with long sustained notes.
|
||||
- Part level meters would suddenly snap to a lower level if a quiet note followed a loud note instead of falling off.
|
||||
- Possibility of overflowing text in HD44780 Print() function.
|
||||
|
||||
## [0.6.2] - 2020-10-18
|
||||
|
||||
@@ -228,9 +228,7 @@ As with all I2C devices, you must know the LCD's I2C address in order for it to
|
||||
|
||||
The SSD1306 controller is found in mini 128x32 and 128x64 OLED displays, which are well-known for their use in FlashFloppy/Gotek devices. They can be found for very little money on eBay and AliExpress.
|
||||
|
||||
Currently, only the 32-pixel high version is supported, although if someone were to send me a 64-pixel high model, I'd be glad to test it and add support!
|
||||
|
||||
These displays usually have an I2C address of `0x3c`.
|
||||
Both 32 and 64 pixel high variants are supported. These displays usually have an I2C address of `0x3c`.
|
||||
|
||||
### Compatibility
|
||||
|
||||
@@ -240,6 +238,7 @@ The following displays and configurations have been confirmed as working by our
|
||||
|----------------|-----------------|----------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------|
|
||||
| BuyDisplay.com | [2002-1 Series] | `type = hd44780_i2c`, `width = 20`, `height = 2`, `i2c_lcd_address = 27` | Very bright and inexpensive 20x2 LCD. Tested by @dwhinham. |
|
||||
| Generic | [128x32 OLED] | `type = ssd1306_i2c`, `width = 128`, `height = 32`, `i2c_lcd_address = 3c` | Extremely cheap yet nice and bright mini OLED. Widely available on AliExpress and eBay. Tested by @dwhinham. |
|
||||
| Generic | [128x64 OLED] | `type = ssd1306_i2c`, `width = 128`, `height = 64`, `i2c_lcd_address = 3c` | Similar to the 32 pixel high OLED, just with more rows. Tested by @dwhinham. |
|
||||
| Raystar | [REC002004B] | `type = hd44780_4bit`, `width = 20`, `height = 4` | High-contrast 20x4 OLED display. Tested by @dwhinham. |
|
||||
|
||||
## 🧠 ROM support
|
||||
@@ -323,7 +322,8 @@ This project, just like [Munt], has no affiliation with Roland Corporation. Use
|
||||
- The [inih] project for a nice, lightweight config file parser.
|
||||
|
||||
[1]: http://www.arvydas.co.uk/2013/07/cheap-usb-midi-cable-some-self-assembly-may-be-required/
|
||||
[128x32 OLED]: https://www.aliexpress.com/item/32661842518.html?spm=a2g0s.9042311.0.0.27424c4dSo7J9L
|
||||
[128x32 OLED]: https://www.aliexpress.com/item/32661842518.html
|
||||
[128x64 OLED]: https://www.aliexpress.com/item/32233334632.html
|
||||
[2]: https://karusisemus.wordpress.com/2017/01/02/cheap-usb-midi-cable-how-to-modify-it/
|
||||
[3]: http://www.fm-alive.com/Pages/DXMidi.aspx
|
||||
[2002-1 Series]: https://www.buydisplay.com/character-lcd-display-module/20x2-character
|
||||
|
||||
@@ -52,20 +52,20 @@ protected:
|
||||
// 20 characters plus null terminator
|
||||
static constexpr size_t TextBufferLength = 20 + 1;
|
||||
|
||||
// MIDI velocity range [1-127] to bar graph height range [0-16] scale factor
|
||||
static constexpr float VelocityScale = 16.f / (127.f - 1.f);
|
||||
|
||||
static constexpr unsigned MessageDisplayTimeMillis = 200;
|
||||
static constexpr unsigned TimbreDisplayTimeMillis = 1200;
|
||||
|
||||
static constexpr float BarFalloff = 1.0f / 16.0f;
|
||||
static constexpr float PeakFalloff = 1.0f / 64.0f;
|
||||
|
||||
TState m_State;
|
||||
unsigned m_nStateTime;
|
||||
|
||||
// MT-32 state
|
||||
char m_TextBuffer[TextBufferLength];
|
||||
u8 m_nPreviousMasterVolume;
|
||||
u8 m_PartLevels[9];
|
||||
u8 m_PeakLevels[9];
|
||||
float m_PartLevels[9];
|
||||
float m_PeakLevels[9];
|
||||
u8 m_PeakTimes[9];
|
||||
};
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ private:
|
||||
void ClearPixel(u8 nX, u8 nY);
|
||||
void DrawChar(char chChar, u8 nCursorX, u8 nCursorY, bool bInverted = false, bool bDoubleWidth = false);
|
||||
|
||||
void DrawPartLevels(u8 nRow, bool bDrawPeaks = true);
|
||||
void DrawPartLevels(u8 nFirstRow, bool bDrawPeaks = true);
|
||||
|
||||
CI2CMaster* m_pI2CMaster;
|
||||
u8 m_nAddress;
|
||||
|
||||
+15
-1
@@ -35,13 +35,27 @@
|
||||
|
||||
namespace Utility
|
||||
{
|
||||
// Templated function for clamping a value between a minimum and a maximum{
|
||||
// Templated function for clamping a value between a minimum and a maximum
|
||||
template <class T>
|
||||
static inline T Clamp(T value, T min, T max)
|
||||
{
|
||||
return (value < min) ? min : (value > max) ? max : value;
|
||||
}
|
||||
|
||||
// Templated function for taking the minimum of two values
|
||||
template <class T>
|
||||
static inline T Min(T lhs, T rhs)
|
||||
{
|
||||
return lhs < rhs ? lhs : rhs;
|
||||
}
|
||||
|
||||
// Templated function for taking the maximum of two values
|
||||
template <class T>
|
||||
static inline T Max(T lhs, T rhs)
|
||||
{
|
||||
return lhs > rhs ? lhs : rhs;
|
||||
}
|
||||
|
||||
// Return number of elements in an array
|
||||
template<class T, size_t N>
|
||||
constexpr size_t ArraySize(const T(&)[N]) { return N; }
|
||||
|
||||
+1
-1
@@ -168,7 +168,7 @@ width = 128
|
||||
#
|
||||
# Same characters vs. pixels considerations as for width.
|
||||
#
|
||||
# Values: 2-32 (2*)
|
||||
# Values: 2-64 (2*)
|
||||
height = 32
|
||||
|
||||
# Set address (hexadecimal) of I2C LCD.
|
||||
|
||||
+6
-4
@@ -224,7 +224,8 @@ void CHD44780Base::DrawPartLevelsSingle(u8 nRow)
|
||||
|
||||
for (u8 i = 0; i < 9; ++i)
|
||||
{
|
||||
lineBuf[i * 2] = BarChars[m_PartLevels[i] / 2];
|
||||
const u8 charIndex = static_cast<u8>(m_PartLevels[i] * 8);
|
||||
lineBuf[i * 2] = BarChars[charIndex];
|
||||
lineBuf[i * 2 + 1] = ' ';
|
||||
}
|
||||
|
||||
@@ -240,15 +241,16 @@ void CHD44780Base::DrawPartLevelsDouble(u8 nFirstRow)
|
||||
|
||||
for (u8 i = 0; i < 9; ++i)
|
||||
{
|
||||
if (m_PartLevels[i] > 8)
|
||||
const u8 partLevel = static_cast<u8>(m_PartLevels[i] * 16);
|
||||
if (partLevel > 8)
|
||||
{
|
||||
line1Buf[i * 2] = BarChars[m_PartLevels[i] - 8];
|
||||
line1Buf[i * 2] = BarChars[partLevel - 8];
|
||||
line2Buf[i * 2] = BarChars[8];
|
||||
}
|
||||
else
|
||||
{
|
||||
line1Buf[i * 2] = BarChars[0];
|
||||
line2Buf[i * 2] = BarChars[m_PartLevels[i]];
|
||||
line2Buf[i * 2] = BarChars[partLevel];
|
||||
}
|
||||
|
||||
line1Buf[i * 2 + 1] = line2Buf[i * 2 + 1] = ' ';
|
||||
|
||||
+14
-11
@@ -24,6 +24,7 @@
|
||||
|
||||
#include "lcd/mt32lcd.h"
|
||||
#include "mt32synth.h"
|
||||
#include "utility.h"
|
||||
|
||||
CMT32LCD::CMT32LCD()
|
||||
: m_State(TState::DisplayingPartStates),
|
||||
@@ -112,10 +113,17 @@ void CMT32LCD::UpdatePartLevels(const CMT32SynthBase& Synth)
|
||||
u32 partStates = Synth.GetPartStates();
|
||||
for (u8 i = 0; i < 9; ++i)
|
||||
{
|
||||
// If part active
|
||||
if ((partStates >> i) & 1)
|
||||
m_PartLevels[i] = floor(VelocityScale * Synth.GetVelocityForPart(i)) + 0.5f;
|
||||
else if (m_PartLevels[i] > 0)
|
||||
--m_PartLevels[i];
|
||||
{
|
||||
// MIDI velocity range [0-127] to normalized range [0-1]
|
||||
float partLevel = Synth.GetVelocityForPart(i) / 127.0f;
|
||||
|
||||
if (partLevel >= m_PartLevels[i])
|
||||
m_PartLevels[i] = partLevel;
|
||||
}
|
||||
else
|
||||
m_PartLevels[i] = Utility::Max(m_PartLevels[i] - BarFalloff, 0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,19 +131,14 @@ void CMT32LCD::UpdatePeakLevels()
|
||||
{
|
||||
for (u8 i = 0; i < 9; ++i)
|
||||
{
|
||||
if (m_PartLevels[i] > m_PeakLevels[i])
|
||||
if (m_PartLevels[i] >= m_PeakLevels[i])
|
||||
{
|
||||
m_PeakLevels[i] = m_PartLevels[i];
|
||||
m_PeakTimes[i] = 100;
|
||||
}
|
||||
else if (m_PartLevels[i] > 0)
|
||||
--m_PartLevels[i];
|
||||
|
||||
if (m_PeakTimes[i] == 0 && m_PeakLevels[i] > 0)
|
||||
{
|
||||
--m_PeakLevels[i];
|
||||
m_PeakTimes[i] = 3;
|
||||
}
|
||||
if (m_PeakTimes[i] == 0 && m_PeakLevels[i] > 0.0f)
|
||||
m_PeakLevels[i] = Utility::Max(m_PeakLevels[i] - PeakFalloff, 0.0f);
|
||||
else
|
||||
--m_PeakTimes[i];
|
||||
}
|
||||
|
||||
+88
-53
@@ -86,6 +86,12 @@ namespace
|
||||
constexpr auto FontSingle = Font<Utility::ArraySize(Font6x8), decltype(SingleColumn)>(Font6x8, SingleColumn);
|
||||
constexpr auto FontDouble = Font<Utility::ArraySize(Font6x8), decltype(DoubleColumn)>(Font6x8, DoubleColumn);
|
||||
|
||||
// Drawing constants
|
||||
constexpr u8 Width = 128;
|
||||
constexpr u8 BarWidth = 12;
|
||||
constexpr u8 BarSpacing = 2;
|
||||
constexpr u8 BarOffset = 2;
|
||||
|
||||
CSSD1306::CSSD1306(CI2CMaster *pI2CMaster, u8 nAddress, u8 nHeight, TLCDRotation Rotation)
|
||||
: CMT32LCD(),
|
||||
m_pI2CMaster(pI2CMaster),
|
||||
@@ -105,48 +111,51 @@ bool CSSD1306::Initialize()
|
||||
if (!(m_nHeight == 32 || m_nHeight == 64))
|
||||
return false;
|
||||
|
||||
const u8 segRemap = m_Rotation == TLCDRotation::Inverted ? 0xA0 : 0xA1;
|
||||
const u8 comScanDir = m_Rotation == TLCDRotation::Inverted ? 0xC0 : 0xC8;
|
||||
const u8 pageAddrRange = m_nHeight == 32 ? 0x03 : 0x07;
|
||||
const u8 segRemap = m_Rotation == TLCDRotation::Inverted ? 0xA0 : 0xA1;
|
||||
const u8 comScanDir = m_Rotation == TLCDRotation::Inverted ? 0xC0 : 0xC8;
|
||||
const u8 multiplexRatio = static_cast<u8>(m_nHeight - 1);
|
||||
const u8 comPins = m_nHeight == 32 ? 0x02 : 0x12;
|
||||
|
||||
const u8 initSequence[] =
|
||||
{
|
||||
0xAE, // Screen off
|
||||
0x81, // Set contrast
|
||||
0x7F, // 00-FF, default to half
|
||||
0xAE, // Screen off
|
||||
0x81, // Set contrast
|
||||
0x7F, // 00-FF, default to half
|
||||
|
||||
0xA6, // Normal display
|
||||
0xA6, // Normal display
|
||||
|
||||
0x20, // Set memory addressing mode
|
||||
0x0, // 00 = horizontal
|
||||
0x21, // Set column start and end address
|
||||
0x20, // Set memory addressing mode
|
||||
0x0, // 00 = horizontal
|
||||
0x21, // Set column start and end address
|
||||
0x00,
|
||||
0x7F,
|
||||
0x22, // Set page address range
|
||||
0x22, // Set page address range
|
||||
0x00,
|
||||
0x03,
|
||||
pageAddrRange,
|
||||
|
||||
segRemap, // Set segment remap
|
||||
0xA8, // Set multiplex ratio
|
||||
0x1F, // Screen height - 1 (31)
|
||||
segRemap, // Set segment remap
|
||||
0xA8, // Set multiplex ratio
|
||||
multiplexRatio, // Screen height - 1
|
||||
|
||||
comScanDir, // Set COM output scan direction
|
||||
0xD3, // Set display offset
|
||||
0x00, // None
|
||||
0xDA, // Set com pins hardware configuration
|
||||
0x02, // Alternate COM config and disable COM left/right
|
||||
comScanDir, // Set COM output scan direction
|
||||
0xD3, // Set display offset
|
||||
0x00, // None
|
||||
0xDA, // Set com pins hardware configuration
|
||||
comPins, // Alternate COM config and disable COM left/right
|
||||
|
||||
0xD5, // Set display oscillator
|
||||
0x80, // Default value
|
||||
0xD9, // Set precharge period
|
||||
0x22, // Default value
|
||||
0xDB, // Set VCOMH deselected level
|
||||
0x20, // Default value
|
||||
0xD5, // Set display oscillator
|
||||
0x80, // Default value
|
||||
0xD9, // Set precharge period
|
||||
0x22, // Default value
|
||||
0xDB, // Set VCOMH deselected level
|
||||
0x20, // Default value
|
||||
|
||||
0x8D, // Set charge pump
|
||||
0x14, // VCC generated by internal DC/DC circuit
|
||||
0x8D, // Set charge pump
|
||||
0x14, // VCC generated by internal DC/DC circuit
|
||||
|
||||
0xA4, // Resume to RAM content display
|
||||
0xAF // Set display on
|
||||
0xA4, // Resume to RAM content display
|
||||
0xAF // Set display on
|
||||
};
|
||||
|
||||
u8 buffer[] = {0x80, 0};
|
||||
@@ -187,6 +196,7 @@ void CSSD1306::ClearPixel(u8 nX, u8 nY)
|
||||
|
||||
void CSSD1306::DrawChar(char chChar, u8 nCursorX, u8 nCursorY, bool bInverted, bool bDoubleWidth)
|
||||
{
|
||||
size_t rowOffset = nCursorY * Width * 2;
|
||||
size_t columnOffset = nCursorX * (bDoubleWidth ? 12 : 6) + 5;
|
||||
|
||||
// FIXME: Won't be needed when the full font is implemented in font6x8.h
|
||||
@@ -208,47 +218,71 @@ void CSSD1306::DrawChar(char chChar, u8 nCursorX, u8 nCursorY, bool bInverted, b
|
||||
size_t offset = rowOffset + columnOffset + (bDoubleWidth ? i * 2 : i);
|
||||
|
||||
m_Framebuffer[offset] = fontColumn & 0xFF;
|
||||
m_Framebuffer[offset + 128] = (fontColumn >> 8) & 0xFF;
|
||||
m_Framebuffer[offset + Width] = (fontColumn >> 8) & 0xFF;
|
||||
if (bDoubleWidth)
|
||||
{
|
||||
m_Framebuffer[offset + 1] = m_Framebuffer[offset];
|
||||
m_Framebuffer[offset + 128 + 1] = m_Framebuffer[offset + 128];
|
||||
m_Framebuffer[offset + Width + 1] = m_Framebuffer[offset + Width];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CSSD1306::DrawPartLevels(u8 nRow, bool bDrawPeaks)
|
||||
void CSSD1306::DrawPartLevels(u8 nFirstRow, bool bDrawPeaks)
|
||||
{
|
||||
size_t rowOffset = nRow * 128;
|
||||
const size_t firstPageOffset = nFirstRow * Width;
|
||||
const u8 totalPages = m_nHeight / 8 - 2;
|
||||
const u8 barHeight = m_nHeight - 8 * 2;
|
||||
|
||||
// For each part
|
||||
for (u8 i = 0; i < 9; ++i)
|
||||
{
|
||||
// Bar graphs
|
||||
u8 topVal, bottomVal;
|
||||
if (m_PartLevels[i] > 8)
|
||||
{
|
||||
topVal = 0xFF << (8 - (m_PartLevels[i] - 8));
|
||||
bottomVal = 0xFF;
|
||||
}
|
||||
else
|
||||
{
|
||||
topVal = 0x00;
|
||||
bottomVal = 0xFF << (8 - (m_PartLevels[i]));
|
||||
}
|
||||
u8 pageValues[totalPages];
|
||||
const u8 partLevelPixels = m_PartLevels[i] * barHeight;
|
||||
const u8 peakLevelPixels = m_PeakLevels[i] * barHeight;
|
||||
const u8 fullPages = partLevelPixels / 8;
|
||||
const u8 remainder = partLevelPixels % 8;
|
||||
|
||||
for (u8 j = 0; j < fullPages; ++j)
|
||||
pageValues[j] = 0xFF;
|
||||
|
||||
for (u8 j = fullPages; j < totalPages; ++j)
|
||||
pageValues[j] = 0;
|
||||
|
||||
if (remainder)
|
||||
pageValues[fullPages] = 0xFF << (8 - remainder);
|
||||
|
||||
// Peak meters
|
||||
if (bDrawPeaks)
|
||||
if (bDrawPeaks && peakLevelPixels)
|
||||
{
|
||||
if (m_PeakLevels[i] > 8)
|
||||
topVal |= 1 << (8 - (m_PeakLevels[i] - 8));
|
||||
const u8 peakPage = peakLevelPixels / 8;
|
||||
const u8 remainder = peakLevelPixels % 8;
|
||||
|
||||
if (remainder)
|
||||
pageValues[peakPage] |= 1 << 8 - remainder;
|
||||
else
|
||||
bottomVal |= 1 << (8 - (m_PeakLevels[i]));
|
||||
pageValues[peakPage - 1] |= 1;
|
||||
}
|
||||
|
||||
for (u8 j = 0; j < 12; ++j)
|
||||
// For each bar column
|
||||
for (u8 j = 0; j < BarWidth; ++j)
|
||||
{
|
||||
m_Framebuffer[rowOffset + i * 14 + j + 3] = topVal;
|
||||
m_Framebuffer[rowOffset + i * 14 + j + 128 + 3] = bottomVal;
|
||||
// For each bar row
|
||||
for (u8 k = 0; k < totalPages; ++k)
|
||||
{
|
||||
size_t offset = firstPageOffset;
|
||||
|
||||
// Start BarOffset pixels from the left
|
||||
offset += BarOffset;
|
||||
|
||||
// Start from bottom-most page
|
||||
offset += (totalPages - 1) * Width - k * Width;
|
||||
|
||||
// i'th bar + j'th bar column
|
||||
offset += i * (BarWidth + BarSpacing) + j;
|
||||
|
||||
// +1 to skip 0x40 byte
|
||||
m_Framebuffer[offset + 1] = pageValues[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -285,6 +319,7 @@ void CSSD1306::Update(const CMT32SynthBase& Synth)
|
||||
UpdatePeakLevels();
|
||||
|
||||
DrawPartLevels(0);
|
||||
Print(m_TextBuffer, 0, 1, true);
|
||||
u8 statusRow = m_nHeight == 32 ? 1 : 3;
|
||||
Print(m_TextBuffer, 0, statusRow, true);
|
||||
WriteFramebuffer();
|
||||
}
|
||||
+2
-2
@@ -195,8 +195,8 @@ u8 CMT32SynthBase::GetVelocityForPart(u8 nPart) const
|
||||
u8 velocities[MT32Emu::DEFAULT_MAX_PARTIALS];
|
||||
u32 playingNotes = m_pSynth->getPlayingNotes(nPart, keys, velocities);
|
||||
|
||||
u8 maxVelocity = velocities[0];
|
||||
for (u32 i = 1; i < playingNotes; ++i)
|
||||
u8 maxVelocity = 0;
|
||||
for (u32 i = 0; i < playingNotes; ++i)
|
||||
if (velocities[i] > maxVelocity)
|
||||
maxVelocity = velocities[i];
|
||||
|
||||
|
||||
Reference in new issue
Block a user