Skip to content

Commit

Permalink
Add support for Yamaha SysEx text/bitmap messages
Browse files Browse the repository at this point in the history
  • Loading branch information
dwhinham committed Dec 12, 2021
1 parent e099575 commit e5b4403
Show file tree
Hide file tree
Showing 6 changed files with 159 additions and 69 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Experimental embedded FTP server for performing updates/config changes without replacing the SD card (new configuration file options).
* This FTP server is a very basic implementation which DOES NOT feature any kind of transport layer security/encryption. Therefore, you should NOT enable this feature on a public network or expose the Raspberry Pi to the Internet.
* The FTP server is disabled by default.
- Support for Yamaha MU-series SysEx text messages, and bitmap messages when using a graphical display.

### Fixed

Expand Down
42 changes: 24 additions & 18 deletions include/lcd/ui.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,10 @@ class CSynthBase;
class CUserInterface
{
public:
enum class TState
enum class TSysExDisplayMessage
{
None,
DisplayingMessage,
DisplayingSpinnerMessage,
DisplayingImage,
DisplayingSC55Text,
DisplayingSC55Dots,
EnteringPowerSavingMode,
InPowerSavingMode
Roland,
Yamaha,
};

CUserInterface();
Expand All @@ -52,8 +46,8 @@ class CUserInterface
void ShowSystemMessage(const char* pMessage, bool bSpinner = false);
void ClearSpinnerMessage();
void DisplayImage(TImage Image);
void ShowSC55Text(const u8* pMessage, size_t nSize, u8 nOffset);
void ShowSC55Dots(const u8* pData);
void ShowSysExText(TSysExDisplayMessage Type, const u8* pMessage, size_t nSize, u8 nOffset);
void ShowSysExBitmap(TSysExDisplayMessage Type, const u8* pData, size_t nSize);
void EnterPowerSavingMode();
void ExitPowerSavingMode();

Expand All @@ -63,18 +57,29 @@ class CUserInterface
static void DrawChannelLevels(CLCD& LCD, u8 nBarHeight, float* pChannelLevels, float* pPeakLevels, u8 nChannels, bool bDrawBarBases);

private:
enum class TState
{
None,
DisplayingMessage,
DisplayingSpinnerMessage,
DisplayingImage,
DisplayingSysExText,
DisplayingSysExBitmap,
EnteringPowerSavingMode,
InPowerSavingMode
};

bool UpdateScroll(CLCD& LCD, unsigned int nTicks);
bool DrawSystemState(CLCD& LCD) const;
void DrawSC55Dots(CLCD& LCD, u8 nFirstRow, u8 nRows) const;
void DrawSysExText(CLCD& LCD, u8 nFirstRow) const;
void DrawSysExBitmap(CLCD& LCD, u8 nFirstRow, u8 nRows) const;

static void DrawChannelLevelsCharacter(CLCD& LCD, u8 nRows, u8 nBarOffsetX, u8 nBarYOffset, u8 nBarSpacing, const float* pChannelLevels, u8 nChannels, bool bDrawBarBases);
static void DrawChannelLevelsGraphical(CLCD& LCD, u8 nBarOffsetX, u8 nBarYOffset, u8 nBarWidth, u8 nBarHeight, u8 nBarSpacing, const float* pChannelLevels, const float* pPeakLevels, u8 nChannels, bool bDrawBarBases);

static constexpr size_t SystemMessageTextBufferSize = 256;
static constexpr size_t SC55TextBufferSize = 32 + 1;

// 64 bytes; each byte representing 5 pixels (see p78 of SC-55 manual)
static constexpr size_t SC55PixelBufferSize = 64;
static constexpr size_t SyxExTextBufferSize = 32 + 1;
static constexpr size_t SysExPixelBufferSize = 64;

static constexpr unsigned SystemMessageDisplayTimeMillis = 3000;
static constexpr unsigned SystemMessageSpinnerTimeMillis = 32;
Expand All @@ -88,8 +93,9 @@ class CUserInterface
size_t m_nCurrentSpinnerChar;
TImage m_CurrentImage;
char m_SystemMessageTextBuffer[SystemMessageTextBufferSize];
char m_SC55TextBuffer[SC55TextBufferSize];
u8 m_SC55PixelBuffer[SC55PixelBufferSize];
TSysExDisplayMessage m_SysExDisplayMessageType;
char m_SysExTextBuffer[SyxExTextBufferSize];
u8 m_SysExPixelBuffer[SysExPixelBufferSize];
};

#endif
2 changes: 1 addition & 1 deletion include/synth/yamahasysex.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ enum TYamahaAddress : u32
XGSystemOn = 0x00007E,

DisplayLetter = 0x060000,
DisplayBitmap = 0x060000,
DisplayBitmap = 0x070000,
};

enum TYamahaAddressMask : u32
Expand Down
3 changes: 3 additions & 0 deletions src/lcd/drivers/ssd1306.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,9 @@ void CSSD1306::DrawChar(char chChar, u8 nCursorX, u8 nCursorY, bool bInverted, b
if (chChar == '\xFF')
chChar = '\x80';

else if (chChar < ' ')
chChar = ' ';

for (u8 i = 0; i < 6; ++i)
{
u16 nFontColumn = FontDouble[static_cast<u8>(chChar - ' ')][i];
Expand Down
143 changes: 102 additions & 41 deletions src/lcd/ui.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,9 @@ CUserInterface::CUserInterface()
m_nCurrentSpinnerChar(0),
m_CurrentImage(TImage::None),
m_SystemMessageTextBuffer{'\0'},
m_SC55TextBuffer{'\0'},
m_SC55PixelBuffer{0}
m_SysExDisplayMessageType(TSysExDisplayMessage::Roland),
m_SysExTextBuffer{'\0'},
m_SysExPixelBuffer{0}
{
}

Expand All @@ -54,8 +55,8 @@ bool CUserInterface::UpdateScroll(CLCD& LCD, unsigned int nTicks)
const char* pMessage;
if (m_State == TState::DisplayingMessage)
pMessage = m_SystemMessageTextBuffer;
else if (m_State == TState::DisplayingSC55Text)
pMessage = m_SC55TextBuffer;
else if (m_State == TState::DisplayingSysExText && m_SysExDisplayMessageType == TSysExDisplayMessage::Roland)
pMessage = m_SysExTextBuffer;
else
return false;

Expand Down Expand Up @@ -110,7 +111,7 @@ void CUserInterface::Update(CLCD& LCD, CSynthBase& Synth, unsigned int nTicks)
}

// SC-55 text timeout
else if ((m_State == TState::DisplayingSC55Text && !m_bIsScrolling || m_State == TState::DisplayingSC55Dots) && nDeltaTicks >= Utility::MillisToTicks(SC55DisplayTimeMillis))
else if ((m_State == TState::DisplayingSysExText && !m_bIsScrolling || m_State == TState::DisplayingSysExBitmap) && nDeltaTicks >= Utility::MillisToTicks(SC55DisplayTimeMillis))
{
m_State = TState::None;
m_nStateTime = nTicks;
Expand Down Expand Up @@ -176,26 +177,35 @@ void CUserInterface::DisplayImage(TImage Image)
m_nStateTime = nTicks;
}

void CUserInterface::ShowSC55Text(const u8* pMessage, size_t nSize, u8 nOffset)
void CUserInterface::ShowSysExText(TSysExDisplayMessage Type, const u8* pMessage, size_t nSize, u8 nOffset)
{
if (nOffset + nSize > SC55TextBufferSize - 1)
return;
if (nOffset + nSize > SyxExTextBufferSize - 1)
nSize = SyxExTextBufferSize - 1 - nOffset;

memset(m_SC55TextBuffer, ' ', nOffset);
memcpy(m_SC55TextBuffer + nOffset, pMessage, nSize);
m_SC55TextBuffer[nOffset + nSize] = '\0';
memset(m_SysExTextBuffer, ' ', nOffset);
memcpy(m_SysExTextBuffer + nOffset, pMessage, nSize);
m_SysExTextBuffer[nOffset + nSize] = '\0';

const unsigned nTicks = CTimer::GetClockTicks();
m_State = TState::DisplayingSC55Text;
m_SysExDisplayMessageType = Type;
m_State = TState::DisplayingSysExText;
m_nCurrentScrollOffset = 0;
m_nStateTime = nTicks;
}

void CUserInterface::ShowSC55Dots(const u8* pData)
void CUserInterface::ShowSysExBitmap(TSysExDisplayMessage Type, const u8* pData, size_t nSize)
{
if (nSize == 0)
return;
if (Type == TSysExDisplayMessage::Roland && nSize > 64)
nSize = 64;
else if (Type == TSysExDisplayMessage::Yamaha && nSize > 48)
nSize = 48;

const unsigned nTicks = CTimer::GetClockTicks();
memcpy(m_SC55PixelBuffer, pData, sizeof(m_SC55PixelBuffer));
m_State = TState::DisplayingSC55Dots;
m_SysExDisplayMessageType = Type;
memcpy(m_SysExPixelBuffer, pData, nSize);
m_State = TState::DisplayingSysExBitmap;
m_nStateTime = nTicks;
}

Expand Down Expand Up @@ -318,46 +328,79 @@ bool CUserInterface::DrawSystemState(CLCD& LCD) const

if (LCD.GetType() == CLCD::TType::Graphical)
{
const u8 nMessageRow = nHeight == 32 ? 0 : 1;

if (m_State == TState::DisplayingImage)
LCD.DrawImage(m_CurrentImage);
else if (m_State == TState::DisplayingSC55Dots)
DrawSC55Dots(LCD, 0, nHeight / 8);
else if (m_State == TState::DisplayingSysExBitmap)
DrawSysExBitmap(LCD, 0, nHeight / 8);
else if (m_State == TState::DisplayingSysExText)
DrawSysExText(LCD, nMessageRow);
else
{
const u8 nMessageRow = nHeight == 32 ? 0 : 1;
const char* const pMessage = m_State == TState::DisplayingSC55Text ? m_SC55TextBuffer : m_SystemMessageTextBuffer;
const u8 nOffsetX = CenterMessageOffset(LCD, pMessage);
LCD.Print(pMessage + m_nCurrentScrollOffset, nOffsetX, nMessageRow, true, false);
const u8 nOffsetX = CenterMessageOffset(LCD, m_SystemMessageTextBuffer);
LCD.Print(m_SystemMessageTextBuffer + m_nCurrentScrollOffset, nOffsetX, nMessageRow, true, false);
}
}
else
{
// Character LCD can't display graphics
if (m_State == TState::DisplayingImage || m_State == TState::DisplayingSC55Dots)
if (m_State == TState::DisplayingImage || m_State == TState::DisplayingSysExBitmap)
return false;

const char* const pMessage = m_State == TState::DisplayingSC55Text ? m_SC55TextBuffer : m_SystemMessageTextBuffer;
const u8 nOffsetX = CenterMessageOffset(LCD, pMessage);

if (nHeight == 2)
{
LCD.Print(pMessage + m_nCurrentScrollOffset, nOffsetX, 0, true);
LCD.Print("", 0, 1, true);
}
else if (nHeight == 4)
if (m_State == TState::DisplayingSysExText)
DrawSysExText(LCD, nHeight == 2 ? 0 : 1);
else
{
// Clear top line
LCD.Print("", 0, 0, true);
LCD.Print(pMessage + m_nCurrentScrollOffset, nOffsetX, 1, true);
LCD.Print("", 0, 2, true);
LCD.Print("", 0, 3, true);
const u8 nOffsetX = CenterMessageOffset(LCD, m_SystemMessageTextBuffer);

if (nHeight == 2)
{
LCD.Print(m_SystemMessageTextBuffer + m_nCurrentScrollOffset, nOffsetX, 0, true);
LCD.Print("", 0, 1, true);
}
else if (nHeight == 4)
{
// Clear top line
LCD.Print("", 0, 0, true);
LCD.Print(m_SystemMessageTextBuffer + m_nCurrentScrollOffset, nOffsetX, 1, true);
LCD.Print("", 0, 2, true);
LCD.Print("", 0, 3, true);
}
}
}

return true;
}

void CUserInterface::DrawSC55Dots(CLCD& LCD, u8 nFirstRow, u8 nRows) const
void CUserInterface::DrawSysExText(CLCD& LCD, u8 nFirstRow) const
{
if (m_SysExDisplayMessageType == TSysExDisplayMessage::Roland)
{
// Roland SysEx text messages are single line and can be scrolled
const u8 nOffsetX = CenterMessageOffset(LCD, m_SysExTextBuffer);
LCD.Print(m_SysExTextBuffer + m_nCurrentScrollOffset, nOffsetX, nFirstRow, true, false);
}
else
{
// TODO: API for getting width in pixels/characters for a string
const size_t nCharWidth = LCD.GetType() == CLCD::TType::Graphical ? 20 : LCD.Width();
const u8 nOffsetX = (nCharWidth - 16) / 2;

// Yamaha SysEx text messages are up to 16x2 characters and do not scroll, so split lines
// and center on the LCD
char Buffer[16 + 1];
memcpy(Buffer, m_SysExTextBuffer, 16);
Buffer[16] = '\0';

LCD.Print(Buffer, nOffsetX, nFirstRow, true, false);

if (strlen(m_SysExTextBuffer) > 16)
LCD.Print(m_SysExTextBuffer + 16, nOffsetX, nFirstRow + 1, true, false);
}
}

void CUserInterface::DrawSysExBitmap(CLCD& LCD, u8 nFirstRow, u8 nRows) const
{
const u8 nWidth = LCD.Width();
const u8 nHeight = LCD.Height();
Expand All @@ -368,19 +411,37 @@ void CUserInterface::DrawSC55Dots(CLCD& LCD, u8 nFirstRow, u8 nRows) const
const size_t nOffsetX = (nWidth - 16 * nScaleX) / 2;
const size_t nOffsetY = (nHeight - 16 * nScaleY) / 2;

for (u8 nByte = 0; nByte < sizeof(m_SC55PixelBuffer); ++nByte)
u8 nHeadLength = 0, nHeadPixels = 0, nTailPixels = 0;

if (m_SysExDisplayMessageType == TSysExDisplayMessage::Roland)
{
// SC-55: Max 64 bytes; each byte representing 5 pixels (see p78 of SC-55 manual)
// First 48 bytes have 5 columns of pixels, last 16 bytes have only 1
const u8 nPixels = nByte < 48 ? 5 : 1;
nHeadLength = 48;
nHeadPixels = 5;
nTailPixels = 1;
}
else if (m_SysExDisplayMessageType == TSysExDisplayMessage::Yamaha)
{
// Yamaha: Max 48 bytes; each byte representing 7 pixels (see p16 of MU80 Sound List & MIDI Data book)
// First 32 bytes have 7 columns of pixels, last 16 bytes have only 2
nHeadLength = 32;
nHeadPixels = 7;
nTailPixels = 2;
}

for (u8 nByte = 0; nByte < sizeof(m_SysExPixelBuffer); ++nByte)
{
const u8 nPixels = nByte < nHeadLength ? nHeadPixels : nTailPixels;

for (u8 nPixel = 0; nPixel < nPixels; ++nPixel)
{
const bool bPixelValue = (m_SC55PixelBuffer[nByte] >> (5 - 1 - nPixel)) & 1;
const bool bPixelValue = (m_SysExPixelBuffer[nByte] >> (nHeadPixels - 1 - nPixel)) & 1;

if (!bPixelValue)
continue;

const u8 nPosX = nByte / 16 * 5 + nPixel;
const u8 nPosX = nByte / 16 * nHeadPixels + nPixel;
const u8 nPosY = nByte % 16;

const u8 nScaledX = nOffsetX + nPosX * nScaleX;
Expand Down
Loading

0 comments on commit e5b4403

Please sign in to comment.