Realtime MIDI Player

andriano
andriano аватар
Offline
Зарегистрирован: 20.06.2015

Режим самоизоляции позволил уделить время многим проектам, которые были в планах, но до которых хронически не доходили руки.

Хронологически первым оказался проект MIDI-box - устройства, позволяющего либо формировать поток MIDI команд, либо добавлять свои MIDI команды к существующему потоку. http://arduino.ru/forum/proekty/midi-boxmidi-sintezator

Вероятно, этот же проект окажется и хронологически последним. По крайней мере, в процессе его реализации возникали и возникают новые идеи и новые проекты, способствующие его продвижению либо достаточно обособленные части, допускающие самостоятельную публикацию.

Первая из таких публикаций (кроме основной) - разработанный для этого проекта класс меню: 

http://arduino.ru/forum/proekty/menyu-dlya-dvukhstrochnogo-displeya

https://youtu.be/PyOdQMH7JxU

Следующим является настоящий проект.

Но в планах присутствует еще парочка.

Вопрос - как долго еще продлится самоизоляция...

andriano
andriano аватар
Offline
Зарегистрирован: 20.06.2015

В процессе работы над проектом MIDI-box, подразумевающем как принятие и анализ входного потока команд, так и формирование выходного, вдруг внезапно обнаружился недостаток устройств, которые могли бы создавать нужный входной поток либо принимать выходной.

Собственно, в наличии присутствовали только MIDI клавиатура и компьютер. Компьютер, конечно, устройство довольно универсальное. Особенно, если на нем не только пользоваться общеупотребительным софтом, но и писать свой. Но все равно не слишком удобное.

Поэтому возникла идея параллельной разработки еще двух устройств:

- автономного проигрывателя MIDI-файлов, которое, в отличие от аналогичных устройств, на выходе давало бы не Audio, а MIDI,

- автономного проигрывателя реалтаймового потока MIDI команд, которое на входе получало бы MIDI, а на выходе давало Audio.

Т.е. вместо стандартного одного устройства MIDI-файл->Audio для наладки аппаратуры должно быть два разных: MIDI-файл->Realtime MIDI и Realtime MIDI->Audio.

Прикинув, что второе намного проще, именно с него решил и начать.

За основу был выбран ардуиновский шилд MP3 проигрывателя на VS1053B. Собственно, особенностью данного контроллера является то, что он проектировался для проигрывания не только MP3, но и множества других, в числе которых *.mid. А если загрузить в него небольшую программу, то он способен воспроизводить и раелтаймовое MIDI. Эта программа (в двоичных кодах) в И-нете была найдена.

 

Реалтаймовый проигрыватель, собранный на макетке

 

Правда следует сказать, что конструкторы шилда разрабатывали его исключительно для MP3, поэтому даже не потрудились вывести наружу последовательный порт, по которому передается стандартный MIDI сигнал. Пришлось, так же как и программу в кодах, передавать его через SPI, причем, с некоторыми изменениями протокола.

Я думаю, каждый компьютерщик на том или ином отрезке своей бурной жизни предпринимал попытки разгона компьютера. Были модели, которые удавалось разгонять в полтора раза и более, но обычно эффект не превосходил 20-30%. А чип VS1053B допускает разгон штатными средствами в 5 раз, причем в 4.5 раза его допускается разгонять в долговременном режиме. А для MIDI это важно. Ведь декодирование MP3 - это всего два стереоканала. В терминологии музыки - две ноты. Ну, еще постобработка - пусть вдвое тяжелее декодирования, т.е. порядка 6 "голых" нот без постобработки. А если разогнать в 4.5 раза, то уже получится 27 "голых" нот или 23 ноты с постобработкой смикшированного сигнала (цифры, конечно, очень условные, верно показаны лишь порядки величин и общая тенденция). При простом же воспроизведении MP3 файлов нужды в разгоне нет, и чип VS1052B таким образом экономит питание, т.к. он предназначен для работы в том числе и в портативных проигрывателях.

Собственно, вопрос этот возник по попытке воспроизвести на VS1053B оркестрового MIDI файла: значительную часть нот синтезатор просто проглатывал. Разгон исправил ситуацию. Поэтому было решено режим разгона сделать основным режимом устройства, для чего найденную в И-нете последовательность команд пришлось дополнить еще и дополнительными командами разгона - от себя лично.

 

устройство было собрано на заготовке ардуиновского шилда и куске макетки для пайки

 

Ардуиновские шилды имеют ряд неприятных особенностей. Одну из них я уже упоминал - наружу выведены далеко не все нужные сигналы. Другая - все приводится к 5-вольтовым уровням. Т.е. если я работаю с 3.3-вольтовым контроллером, то должен заботиться о сопряжении с преобразователями уровня, которые стоят между 3.3-вольтовым ведомым устройством и внешним разъемом шилда, выдавая наружу 5 Вольт.

Собственно, для конструкции решено было использовать Blue Pill, т.к. при цене где-то между Arduino MINI и arduino NANO он имеет полноценные как USB, так и Serial. И уж намного дешевле Arduino Micro или Leonardo, имеющими нужные порты. Не говоря о Меге, которая озадачивает еще и  размерами.

Так вот, только часть ножек Blue Pill толерантна к 5 В. А в проекте нужно было одновременно использовать Serial, SPI и I2C. Первый - для приема MIDI сигнала, второй - для управления VS1053B, а третий - для отображения на дисплее последних пришедших команд (тоже очень полезно для отладки MIDI аппаратуры). Так что выбор подходящей комбинации оказался не слишком широким.

Вот тут и пришла засада. Сначала собрал и заставил работать все на макетке. Потом - начал разводить плату. Когда я четверть века назад занимался аналоговой техникой, вполне спокойно разводил и травил однослойные платы. При первых же опытах с цифровой (еще в то время) обнаружил, что одного слоя для цифровых устройств явно недостаточно. Ну а с появлением современных мелковыводных деталей рисовать от руки дорожки лаком для ногтей и вовсе оказалось нереальным (с учетом "изменившегося" за последние четверть века зрения). Поэтому сейчас для всех конструкций использую исключиетльно макетки для пайки (или заготовки ардуиновских шилдов, без которых трудно, т.к. "отцы основатели" позаботились о нестандартном расположении контактов).

Так вот, о засаде: в процессе разводки платы оказалось, что гораздо удобнее изменить комбинацию интерфейсов. Хорошо, решил опробовать это, не разбирая макет, просто переткнув провода. Полдня бился, но так и не сумел заставить работать устройство в новой конфигурации. Решил, что утро вечера мудренее и отложил разбирательство на завтра. Конечно, быстрее, проще и дешевле было бы отказаться от идеи переназначения - распаять три лишних провода не весть какая работа, но тут - дело принципа: надо же разобраться!

Вечером перед сном имею тенденцию почитать, что пишут в И-нете. Ну и заодно решил провентилировать возникший вопрос. На одном из англоязычных форумов обнаружил ссылку на errata по похожему случаю, цитирую последнюю: 

2.8.7  I2C1 with SPI1 remapped and used in master mode
Conditions
• I2C1 and SPI1 are clocked.
• SPI1 is remapped.
• I/O port pin PB5 is configured as an alternate function output.
Description
Conflict between the SPI1 MOSI signal and the I2C1 SMBA signal (even if SMBA is not 
used).
Workaround
Do not use SPI1 remapped in master mode and I2C1 together.
When using SPI1 remapped, the I2C1 clock must be disabled.
 
Вот так, а я как раз использую оба эти протокола, именно в мастер моде и попытался применить ремаппинг.
Пришлось откатываться на первый вариант с SPI_2. 
 
 
получилась вот такая типично ардуиновская 4-этажная конструкция
 
После проверки спаянной конструкции полдня еще потратил на то, чтобы нарисовать корпус. Вот сейчас жду, пока 3D-принтер закончит работу, а заодно пишу эту статью.
В очередной раз отметил, что возиться с самим проектом гораздо интереснее, чем описывать результат, а потому описание идет довольно тяжко.
andriano
andriano аватар
Offline
Зарегистрирован: 20.06.2015

Да уж...

Предыдущий пост отправлен 8 мая, код дописан 9 мая, а до того, чтобы завершить публикацию, руки все не доходят.

Итак, схема:

AUDIO OUT - это 3.5 мм гнездо, расположенное на шилде аппаратного синтезатора VS1053B.

Код для последнего - в следующих двух файлах:

VS1053B_MIDI.h

#ifndef VS1053B_MIDI_H
#define VS1053B_MIDI_H 

#include "Arduino.h" 

class VS1053B_device {
  public:
    VS1053B_device() {};
    ~VS1053B_device() {};
    void setup(bool PrintON);
    void talkMIDI(byte cmd, byte data1, byte data2);   //Sends a MIDI command/data. Doesn't check to see that cmd is greater than 127, or that data values are less than 127
    void noteOn(byte channel, byte note, byte attack_velocity);   //Send a MIDI note-on message.  Like pressing a piano key. channel ranges from 0-15
    void noteOff(byte channel, byte note, byte release_velocity);   //Send a MIDI note-off message.  Like releasing a piano key
    void sendMIDI(byte data);
  private:
    void WriteRegister(unsigned char addressbyte, unsigned char highbyte, unsigned char lowbyte);    //Write to VS10xx register. SCI: Data transfers are always 16bit.
    void LoadUserCode(void);   // Plugin to put VS10XX into realtime MIDI mode.  Originally from http://www.vlsi.fi/fileadmin/software/VS10XX/vs1053b-rtmidistart.zip
};

extern VS1053B_device VS1053B;

#endif // VS1053B_MIDI_H

и VS1053B_MIDI.cpp

#include "VS1053B_MIDI.h"
#include <SPI.h>

VS1053B_device VS1053B;
SPIClass SPI_2(2); //Create an instance of the SPI Class called SPI_2 that uses the 2nd SPI Port 

#define VS_START_SPEED 1000000 // предел по дэйташиту CLKI/7 (=1.76 при 12.288 МГц)
#define VS_SPEED       4000000 // предел по дэйташиту CLKI/7 (=7.9 при 55.3 МГц)

// SCK2  - PB13  // 13 
// MISO2 - PB14  // 12 
// MOSI2 - PB15  // 11 
// SD-CS - PB8   //  9 SD-card
#define VS_RESET   PB9   // 8 Reset is active low
#define VS_XDCS    PB3   // 7 Data Chip Select / BSYNC Pin
#define VS_XCS     PB4   // 6 Control Chip Select Pin (for accessing SPI Control/Status registers)
#define VS_DREQ    PA15  // 2 Data Request Pin: Player asks for more data


SPISettings VS1053B_START(VS_START_SPEED, MSBFIRST, SPI_MODE0);
SPISettings VS1053B_HiFreq(VS_SPEED, MSBFIRST, SPI_MODE0);

   //Write to VS10xx register
   //SCI: Data transfers are always 16bit. When a new SCI operation comes in DREQ goes low. We then have to wait for DREQ to go high again.
   //XCS should be low for the full duration of operation.
void VS1053B_device::WriteRegister(unsigned char addressbyte, unsigned char highbyte, unsigned char lowbyte){
  while(!digitalRead(VS_DREQ)) ; //Wait for DREQ to go high indicating IC is available
  SPI_2.beginTransaction (VS1053B_START);
  digitalWrite(VS_XCS, LOW); //Select control

  //SCI consists of instruction byte, address byte, and 16-bit data word.
  SPI_2.transfer(0x02); //Write instruction
  SPI_2.transfer(addressbyte);
  SPI_2.transfer(highbyte);
  SPI_2.transfer(lowbyte);
  while(!digitalRead(VS_DREQ)) ; //Wait for DREQ to go high indicating command is complete
  digitalWrite(VS_XCS, HIGH); //Deselect Control
  SPI_2.endTransaction   ();
}

   // Plugin to put VS10XX into realtime MIDI mode
   // Originally from http://www.vlsi.fi/fileadmin/software/VS10XX/vs1053b-rtmidistart.zip
   // Permission to reproduce here granted by VLSI solution.
const PROGMEM unsigned short sVS1053b_Realtime_MIDI_Plugin[] = { /* [28] Compressed plugin */
  0x0007, 0x0001, 0x8050, 0x0006, 0x0014, 0x0030, 0x0715, 0xb080, /*    0 */
  0x3400, 0x0007, 0x9255, 0x3d00, 0x0024, 0x0030, 0x0295, 0x6890, /*    8 */
  0x3400, 0x0030, 0x0495, 0x3d00, 0x0024, 0x2908, 0x4d40, 0x0030, /*   10 */
  0x0200, 0x000a, 0x0001, 0x0050, 0x0003, 0x0001, 0xd800      // добавлена команда увел.частоты: d800->r3 (x4.5+1.5)
};              // 0x9800: x3.5+1.5,   0xa00: x4.0+0.0,   0xb800: x4.0+1.5,   0xc00: x4.5+0.0,   0xe00: x5.0+0.0

void VS1053B_device::LoadUserCode(void) {
  int i = 0;
  while (i<sizeof(sVS1053b_Realtime_MIDI_Plugin)/sizeof(sVS1053b_Realtime_MIDI_Plugin[0])) {
    unsigned short addr, n, val; // pgm_read_word_near(&glideStepArray[glide])
    addr = pgm_read_word_near(&sVS1053b_Realtime_MIDI_Plugin[i++]);
    n = pgm_read_word_near(&sVS1053b_Realtime_MIDI_Plugin[i++]);
    while (n--) {
      val = pgm_read_word_near(&sVS1053b_Realtime_MIDI_Plugin[i++]);
      WriteRegister(addr, val >> 8, val & 0xFF);
    }
  }
}

   //Sends a MIDI command/data. Doesn't check to see that cmd is greater than 127, or that data values are less than 127
void VS1053B_device::talkMIDI(byte cmd, byte data1, byte data2) {
  while (!digitalRead(VS_DREQ));  // Wait for chip to be ready (Unlikely to be an issue with real time MIDI)
  SPI_2.beginTransaction (VS1053B_HiFreq);
  digitalWrite(VS_XDCS, LOW);
  SPI_2.transfer(0);
  SPI_2.transfer(cmd);
  if( (cmd & 0xF0) <= 0xB0 || (cmd & 0xF0) >= 0xE0) {
    SPI_2.transfer(0);
    SPI_2.transfer(data1);
    SPI_2.transfer(0);
    SPI_2.transfer(data2);
  } else {
    SPI_2.transfer(0);
    SPI_2.transfer(data1);
  }
  digitalWrite(VS_XDCS, HIGH);
  SPI_2.endTransaction ();
}

void VS1053B_device::setup(bool PrintON) {
  if (PrintON) Serial.println(F("VS1053B_setup Start"));
  digitalWrite(VS_XCS, HIGH); //Deselect Control
  pinMode(VS_XCS, OUTPUT);

  digitalWrite(VS_XDCS, HIGH); //Deselect Data
  pinMode(VS_XDCS, OUTPUT);

  digitalWrite(VS_RESET, LOW); //Put VS1053 into hardware reset
  pinMode(VS_RESET, OUTPUT);

  pinMode(VS_DREQ, INPUT);

  SPI_2.begin();
  //From page 12 of datasheet, max SCI reads are CLKI/7. Input clock is 12.288MHz. 
  //Internal clock multiplier is 1.0x after power up. 
  //Therefore, max SPI speed is 1.75MHz. We will use 1MHz to be safe.
  
  SPI_2.transfer(0xFF); //Throw a dummy byte at the bus

  delayMicroseconds(1);
  digitalWrite(VS_RESET, HIGH); //Bring up VS1053
  
  LoadUserCode(); // if USE_PATCH_INIT
  delay(1000); // (4)

  talkMIDI(0xB0, 0x79, 127); // Reset All Controllers
  talkMIDI(0xB0, 0x7B, 0); // All Notes Off
  talkMIDI(0xB0, 0x5B, 0x41); // External effects depth
  talkMIDI(0xB0, 0x5D, 0); // Chorus depth
  talkMIDI(0xB0, 0x40, 0); // Sustain pedal
  talkMIDI(0xB0, 0x00, 0); // Bank select
  talkMIDI(0xB0, 0x01, 0); // Modulation Wheel
  talkMIDI(0xB0, 0x0A, 0x37); // Pan
  talkMIDI(0xC0, 0x38, 0); // Program number
  talkMIDI(0xE0, 0x00, 0x40); // Pitch Wheel
  talkMIDI(0xB0, 0x07, 100); //  Main Volume
  talkMIDI(0xB0, 0, 0x00); //Default bank GM1
  if (PrintON) Serial.println("VS1053B_setup End");
}

//Send a MIDI note-on message.  Like pressing a piano key
//channel ranges from 0-15
void VS1053B_device::noteOn(byte channel, byte note, byte attack_velocity) {
  talkMIDI( (0x90 | channel), note, attack_velocity);
}

//Send a MIDI note-off message.  Like releasing a piano key
void VS1053B_device::noteOff(byte channel, byte note, byte release_velocity) {
  talkMIDI( (0x80 | channel), note, release_velocity);
}

void VS1053B_device::sendMIDI(byte data)
{
  while (!digitalRead(VS_DREQ));  // Wait for chip to be ready (Unlikely to be an issue with real time MIDI)
  SPI_2.beginTransaction (VS1053B_HiFreq);
  digitalWrite(VS_XDCS, LOW);
  SPI_2.transfer(0);
  SPI_2.transfer(data);
  digitalWrite(VS_XDCS, HIGH);
  SPI_2.endTransaction ();
} 

О разгоне модуля я уже говорил, о том, что с ним, в отсутствие распаянного порта, приходится общаться по SPI, - тоже. Так вот, максимальная частота SPI сильно зависит от частоты работы самого модуля. Поэтому сначала в него на частоте SPI 1 MHz загружается программа, осуществляющая помимо прочего и разгон, а потом, с уже разогнанным, общение происходит на частоте SPI 4 MHz.

В "комплект" модулей входят также файлы MIDIfn.h и MIDIfn.cpp, опубликованные в 6-м сообщении связанного проекта: http://arduino.ru/forum/proekty/midi-boxmidi-sintezator , поэтому повторять здесь их не буду. Собственно, в этих файла - базовый класс MIDI, а его наследник, используемый в данном проекте выглядит так:

файл MIDIplay.h

#ifndef MIDIPLAY_H // производный от MIDIengine класс - добавлена работа с органами управления
#define MIDIPLAY_H

#include <Arduino.h>
#include "MIDIfn.h"

#define timeInterval 10 // периодичность между отправками значений изменяющегося контрола - 14 мс (реально получается на 3-5 мс больше установленной)
#define Debug_MIDIbox_CTRL false    // печать для отладки контроллеров

class MIDIplay;
extern MIDIplay MIDI;

class MIDIplay : public MIDIengine 
{
public:
    // команды реализации
    MIDIplay() : MIDIengine() {}
//    ~MIDIbox() : ~MIDIengine() {}
//    void Check();
//    void AllNotesOff() {};
    virtual void ControlChange(uint8_t control, uint8_t value, uint8_t channel);
    virtual void ProgramChange(uint8_t program, uint8_t channel);
    virtual void NoteOff(uint8_t note, uint8_t channel);
    virtual void NoteOn(uint8_t note, uint8_t vel, uint8_t channel);
    virtual void PitchWheel(uint8_t loByte, uint8_t hiByte, uint8_t channel);
protected:
    // MIDI-команды (с номерами)
//    void KeyPressure(uint8_t note, uint8_t pressure, uint8_t channel);
    void AfterTouch(uint8_t value, uint8_t channel);
//    void QuarterFrame(uint8_t value);
//    void SongPointer(uint8_t hiByte, uint8_t loByte);
//    void SongSelect(uint8_t song);
//    void TuneRequest();
//    void SystemExclusive(); // (буфер SysEx заполнен) вызывается по команде F7 и любой другой команде кроме RealTime
    virtual void ActSenAction(); // реакция на сообщение ActiveSensing - по умолчанию отсутствует
//    void AddSysExBuffer(uint8_t data) {};
};

#endif // MIDIBOX_H

и MIDIplay.cpp

#include "MIDIplay.h"
#include "VS1053B_MIDI.h" 
#include "screen.h"

#define DEBUG_1

MIDIplay MIDI;

void MIDIplay::NoteOn(uint8_t note, uint8_t vel, uint8_t channel) {
  if (SerialON) Serial.println(F("MIDIplay::NoteOn"));
  VS1053B.noteOn(channel, note, vel);
  screen.addCommand(3, 0x90 | channel, note, vel);
//  MIDIport.write(0x90 | channel);
//  MIDIport.write(note);
//  MIDIport.write(vel);
}

void MIDIplay::NoteOff(uint8_t note, uint8_t channel) { // переделать на 3 параметра: release_velocity
  if (SerialON) Serial.println(F("MIDIplay::NoteOff"));
  VS1053B.noteOff(channel, note, 0);
  screen.addCommand(3, 0x80 | channel, note, 0);
//  MIDIport.write(0x80 | channel);
//  MIDIport.write(note);
//  MIDIport.write(0);
}

void MIDIplay::ControlChange(uint8_t control, uint8_t value, uint8_t channel) {
  if (SerialON) Serial.println(F("MIDIplay::ControlChange"));
  VS1053B.talkMIDI(0xB0 | channel, control, value);
  screen.addCommand(3, 0xB0 | channel, control, value);
//  MIDIport.write(0xB0 | channel);
//  MIDIport.write(control);
//  MIDIport.write(value);
}

void MIDIplay::ProgramChange(uint8_t program, uint8_t channel) {
  if (SerialON) Serial.println(F("MIDIplay::ProgramChange"));
  VS1053B.talkMIDI(0xC0 | channel, program, 0);
  screen.addCommand(2, 0xC0 | channel, program, 0);
//  MIDIport.write(0xC0 | channel);
//  MIDIport.write(program);
}

void MIDIplay::PitchWheel(uint8_t loByte, uint8_t hiByte, uint8_t channel) {   // функция записывает в буфер событие изменения Pitch Bend
  if (SerialON) {
    Serial.print(F("MIDIplay::PitchWheel ch: "));
    Serial.println(channel);
  }
  VS1053B.talkMIDI(0xE0 | channel, loByte, hiByte);
  screen.addCommand(3, 0xE0 | channel, loByte, hiByte);
//  MIDIport.write(0xE0 | channel);
//  MIDIport.write(loByte);
//  MIDIport.write(hiByte);
}

void MIDIplay::ActSenAction() { // реакция на сообщение ActiveSensing - по умолчанию отсутствует
  VS1053B.sendMIDI(0xFE);
  MIDIport.write(0xFE);
}

void MIDIplay::AfterTouch(uint8_t value, uint8_t channel) {
  if (SerialON) Serial.println(F("MIDIplay::ProgramChange"));
  VS1053B.talkMIDI(0xD0 | channel, value, 0);
  screen.addCommand(2, 0xD0 | channel, value, 0);
//  MIDIport.write(0xD0 | channel);
//  MIDIport.write(value);
}

#undef DEBUG_1

 

andriano
andriano аватар
Offline
Зарегистрирован: 20.06.2015

В устройстве используется мой любимый OLED дисплей 128х64, для которого я еще в первые дни своего знакомства с Ардуино написал библиотечку, впоследствии опубликованную: http://arduino.ru/forum/proekty/asoled-kompaktnaya-biblioteka-dlya-oled-displeya-128kh64-s-kirillitsei-utf-8

Но в данном проекте от нее используется по сути только инициализация дисплея. Да и задача стояла простая: показывать 8 последних пришедших MIDI команд. Что удобнее в 16-чном виде. Поэтому и фонт нарисовал специально для этого проекта, состоящий всего из 16 цифр. 

Но работа с растровым дисплеем - занятие довольно медленное. Особенно по последовательному интерфейсу. А еще более особенно, если ради экономии проводов протокол требует передавать дополнительную информацию, в частности, о номере устройства. А совсем уж особенно - если частота работы последовательной шины ограничена величиной 400 кГц. Кстати, последнее меня несколько озадачило: оказалось, библиотека I2C для STM32 поддерживает всего две частоты передачи 100кГц и 400 кГц, причем при любом значении кроме 400000 библиотека устанавливает частоту 100000. Даже на Uno I2C удавалось разогнать почти до 1 МГц, а на Due, я проверял, и сама библиотека и дисплей в частности устойчиво работают на 2 МГц. А вот на STM32 выше 400 кГц - никак. Ну, точнее, со стандартной для него библиотекой. А ковыряться с собственной реализацией I2C для STM32 мне как-то не хотелось - хотелось побыстрее доделать дивайс.

Так вот, о скорости: протокол MIDI сам не очень быстрый - задержка ноты составляет примерно 1 мс. При том, что величины порядка 7-10 мс уже различаются на слух. А вывод на экран - всего 400 Гц. А это порядка 50 мс. В общем, на экран пришлось выводить кусочками по 16 байт. 

В программе предусмотрен текстовый экранный буфер, который поддерживается в актуальном состоянии обработчиком MIDI сообщений, благо, он всего 8 строк по 6 символов (MIDI команда - три 16-ричных числа). Затем в перемешку с опросом входящих MIDI команд происходит постепенное отображение на экран, разделенное на 65 этапов: один из которых - это преобразование текстового буфера в графический - объемом 1 кБайт, а остальные 64 - поблочная (длина блока 16 байт) пересылка этого экранного буфера собственно на дисплей. В результате максимальная дополнительная задержке MIDI команды не превосходит 0.6-0.8 мс, что вполне приемлемо. Ну и частота обновления экрана порядка 20 fps, что также не вызывает возражений.

Работа с экраном, как обычно, сосредоточена в двух файлах с оригинальными названиями:

screen.h

#ifndef SCREEN_H
#define SCREEN_H

#include <Arduino.h>

class screen1306;
extern screen1306 screen;

class screen1306 {
public:
  screen1306(){};
  ~screen1306(){};
  void init();
  void clear();
  void drawDigit(byte row, byte column, byte value);
  void draw();
  void addCommand(byte len, byte cmd, byte parm1, byte parm2);
  void run(); // вызывать на каждом проходе цикла
private:  
  byte screenState; // переменная, управляющая выводом на экран
};

#endif

и screen.cpp

//#include <Arduino.h>
#include <Wire.h>
#include "comm_ctrl.h"
#include "screen.h"
#include "ASOLED.h"
#include "VS1053B_MIDI.h" 

screen1306 screen;

byte buffer[1028] = {0x80, 0x40, 0xFF, 0x01}; // заголовок буфера будет оставаться простоянным, а область изображения - меняться

const byte digBMP[12*16] = {  // таблица знакогенератора для цифр от "0" до "F"
0x7E, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x7E,  // 0
0x0C, 0x0C, 0x1C, 0x3C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C,  // 1
0x7E, 0xC3, 0xC3, 0xC3, 0x03, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC3, 0xFF,  // 2
0x7E, 0xC3, 0xC3, 0xC3, 0x03, 0x0E, 0x03, 0x03, 0xC3, 0xC3, 0xC3, 0x7E,  // 3
0x03, 0x07, 0x0F, 0x1B, 0x33, 0x63, 0xC3, 0xFF, 0x03, 0x03, 0x03, 0x03,  // 4
0xFF, 0xC3, 0xC0, 0xC0, 0xC0, 0xFE, 0x03, 0x03, 0x03, 0xC3, 0xC3, 0x7E,  // 5
0x7E, 0xC3, 0xC3, 0xC0, 0xC0, 0xFE, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x7E,  // 6
0xFF, 0xC3, 0x03, 0x06, 0x06, 0x0C, 0x0C, 0x18, 0x18, 0x30, 0x30, 0x30,  // 7
0x7E, 0xC3, 0xC3, 0xC3, 0xC3, 0x7E, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x7E,  // 8
0x7E, 0xC3, 0xC3, 0xC3, 0xC3, 0x7F, 0x03, 0x03, 0x03, 0xC3, 0xC3, 0x7E,  // 9
0x0F, 0x1B, 0x1B, 0x33, 0x33, 0x63, 0x63, 0xFF, 0xC3, 0xC3, 0xC3, 0xC3,  // A
0x7E, 0xC3, 0xC3, 0xC3, 0xC6, 0xFC, 0xC6, 0xC3, 0xC3, 0xC3, 0xC3, 0x7E,  // B
0x7E, 0xC3, 0xC3, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC3, 0xC3, 0x7E,  // C
0xFC, 0xC6, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC6, 0xFC,  // D
0xFF, 0xC3, 0xC0, 0xC0, 0xC0, 0xFC, 0xC0, 0xC0, 0xC0, 0xC0, 0xC3, 0xFF,  // E
0xFF, 0xC3, 0xC0, 0xC0, 0xC0, 0xFC, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0}; // F

const int dBank[6]  = {7, 6, 5, 3, 2, 1}; // мультипликативное смещение для row
const int dShift[6] = {8, 6, 1, 7, 2, 0}; // аддитивное смещение для row

byte scrChars[48] = {
  16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,  // массив байтов для отображения на экране (если байт >=16, он не рисуется)
  16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16}; // массив байтов для отображения на экране (если байт >=16, он не рисуется)

void screen1306::init(){
  MIDIport.begin(31500);
  LD.init();  //initialze OLED display
  LD.clearDisplay(); 
  if (SerialON) Serial.println("Start");
  delay(150);

  Serial.println(F(" before VS1053B_setup Start"));
  pinMode(LED_ACTSEN_PIN, OUTPUT);
  VS1053B.setup(SerialON);
  Serial.println(F(" after VS1053B_setup Start"));
  delay(200);
  Serial2.begin(31325); 
  VS1053B.noteOn(0, 60, 64);
  VS1053B.noteOn(0, 63, 64);
  VS1053B.noteOn(0, 67, 64);
  delay(2000);
  VS1053B.noteOff(0, 60, 64);
  VS1053B.noteOff(0, 63, 64);
  VS1053B.noteOff(0, 67, 64);

  delay(100);
  VS1053B.talkMIDI(0xC0, 0x13, 0); // Program number 
 //         LD.SetNormalOrientation(); // pins on top 
  LD.SetTurnedOrientation(); // pins on bottom
}

void screen1306::clear() { 
  memset(&buffer[4], 0, 1024); 
};

void screen1306::draw() {
  LD.drawBitmap(buffer, 0, 0, 128, 8);
};

void screen1306::drawDigit(byte row, byte column, byte value) {
  if (value < 16) {
    for (int i = 0; i < 12; i++) { // цикл по строкам сверху
      register uint16_t tmp = digBMP[i + value*12] << dShift[column];
      register int addrL = 6 + dBank[column]*128 + 16*row + i; // 4 + dBank[column]*128 + 2 + 16*row + i
      buffer[addrL]       |= highByte(tmp);
      buffer[addrL - 128] |= lowByte(tmp);
    }
  }
}

void screen1306::addCommand(byte len, byte cmd, byte parm1, byte parm2) {
  if (SerialON) Serial.println("addCommand ");
  for (int i = 0; i < 42; i++)
    scrChars[i] = scrChars[i+6]; // сдвигаем экран вверх
  scrChars[42] = cmd >> 4;
  scrChars[43] = cmd & 0x0F;
  if (1 < len) {
    scrChars[44] = parm1 >> 4;
    scrChars[45] = parm1 & 0x0F;
  } else {
    scrChars[44] = 16;
    scrChars[45] = 16;
  }
  if (2 < len) {
    scrChars[46] = parm2 >> 4;
    scrChars[47] = parm2 & 0x0F;
  } else {
    scrChars[46] = 16;
    scrChars[47] = 16;
  }
}

#define ASA_OLED_DATA_MODE        0x40
#define OLED_ADDRESS              0x3C 

void screen1306::run() { // вызывать на каждом проходе цикла
  for(int i = 0; i < 6; i++)
    for(int j = 0; j < 8; j++)
      drawDigit(j,i,scrChars[j*6 + i]);
  if(screenState < 64) {
    Wire.beginTransmission(OLED_ADDRESS); // begin transmitting
    Wire.write(ASA_OLED_DATA_MODE);//data mode
    for (byte i = 0; i < 16; i++){
      Wire.write(buffer[4 + screenState*16 + i]);
    }
    Wire.endTransmission();    // stop transmitting/////////////////////////////////////////////////////////////
  } else {
    clear();
    for(int i = 0; i < 6; i++)
      for(int j = 0; j < 8; j++)
        drawDigit(j,i,scrChars[j*6 + i]);
  }
  screenState++;
  if (screenState >=65) screenState = 0;
}

Ну и в заключение (какая же статья без картинок!) фото, на котором изображена совместная отладка блоков из двух проектов: MIDI-бокса (слева - поставлен на попа, т.к. снизу к нему подключен логический анализатор) и MIDI-плеера (справа, лежит на столе собранный на макетной плате):

mykaida
mykaida аватар
Offline
Зарегистрирован: 12.07.2018

Большое спасибо за библиотеку VS1053_MIDI!

Небольшое уточнение, на которое в связи со своей невнимательностью убил час. В файле VS1053_MIDI.cpp в 1-й строке стоит 

#include "VS1053B_MIDI.h"

а поскольку и такая библиотека у меня стоит, то компилятор матно ругался на отсутствие объявлений функций. Исправить просто - написать там:

#include "VS1053_MIDI.h"

andriano
andriano аватар
Offline
Зарегистрирован: 20.06.2015

Это моя невнимательность: файлы на самом деле называются VS1053B_MIDI.*. В тексте поправил.

Приношу свои извинения за доставленные проблемы.

mykaida
mykaida аватар
Offline
Зарегистрирован: 12.07.2018

andriano пишет:

Это моя невнимательность: файлы на самом деле называются VS1053B_MIDI.*. В тексте поправил.

Приношу свои извинения за доставленные проблемы.

А мне как раз понравилось предыдущее название. Под ардуинку -  VS1053B_MIDI.*, под STM32 -  VS1053_MIDI.*

Хотя Ваша библиотека поинтереснее. Туда бы команды препроцессора на выбор варианта SPI в зависимости от контроллера - так и ей вообще цены бы не было. Сам в них, к сожалению, не силен.

andriano
andriano аватар
Offline
Зарегистрирован: 20.06.2015

Так наверное, основа у библиотек, как и название, одна и та же. Я ведь не с нуля библиотеку делал, а сначала скачал, что нашел, потом уже переделывал под свои нужды и, кстати, в предыдущем проекте ( http://arduino.ru/forum/proekty/analog-analogovogo-sintezatora ) эта библиотечка использовалась с Мегой 2560.

По поводу препроцессора, так там только с stm32 столько вариантов... Причем, как выяснилось, эти варианты могут еще и зависеть от прочей подключаемой аппаратуры (например, SPI1 на альтернативных пинах вместе с I2C не работает, причем узнать об этом, кроме, разумеется, практики, можно только из errata, - в дэйташите этого нет). В общем, описание универсального случая весьма проблематично.

И выбрать из трех возможных вариантов наиболее приемлемый вариант по умолчанию - тоже проблема, т.к. основные пины для SPI1 не толерантны к 5 Вольтам.

В общем, VS1053B не такое уж распространенное устройство, и если оно используется, то является одним из центральных в системе. А раз так, лучше не держать его в списке универсальных библиотек, а класть каждый раз в папочку конкретного проекта и рихтовать под каждый конкретный случай.

И еще: не люблю директивы препроцессора за то, что они здорово мешают читать исходник.

andriano
andriano аватар
Offline
Зарегистрирован: 20.06.2015

Ну и сегодня завершаю публикацию проекта в том виде, в каком он на сегодняшний день состоялся. В частности, он уже оказался очень полезен для реализации длящихся в настоящее время проектов, описание которых последует через некоторое время.

Осталось всего два файла, при этом размеры их таковы, что можно обойтись без их сворачивания.

Первый - основная программа. Расширение у нее .ino, а назвать ее может каждый, как душенька пожелает. Тем более с именами файлов я в этой теме уже облажался.

#define SPI_HAS_TRANSACTION
#include <Wire.h>
#include <SPI.h>

#include "ASOLED.h"
#include "VS1053B_MIDI.h" 
#include "MIDIplay.h"
#include "screen.h"

void setup() {
  if (SerialON) {
    Serial.begin(115200);
    while(!Serial);
  }
  screen.init();
}

void loop() {
  MIDI.Check();
  screen.run();
  if (SerialON) {
    if(Serial.available()) {
      byte ch = Serial.read();
      switch (ch) {
        case 't':
          MIDI.printStat();
        break;
      }
    }
  }
}

Второй файл - настроечный, называется comm_ctrl.h

#ifndef COMM_CTRL_H
#define COMM_CTRL_H

#include <Arduino.h>

#define MIDIport Serial3
#define LED_ON LOW
#define LED_OFF HIGH
#define LED_ACTSEN_PIN PB12 // pin "Active Sensing"
#define SerialON false

#endif

Само устройство выглядит так:

Светодиод показывает наличие сигнала Active Sensing, а на экране отображаются последние 8 MIDI команд.

У прибора есть еще одна особенность: при включении питания он воспроизводит органным звуком мажорный аккорд (чтобы продемонстрировать свою работоспособность), а затем НЕ возвращается к настройкам по умолчанию. С точки зрения настройки разрабатываемых устройств именно такое поведение оказалось удобным - есть контроль за тем, не "забывает" ли новое устройство послать команды, инициализирующие синтезатор.