суббота, 24 сентября 2016 г.

EV3 Music Station

В нашем новом проекте мы вновь коснемся музыкальной темы. Ранее мы уже строили несколько музыкальных "роботов": NXT-гитару, EV3-гитару, EV3-скрипку, NXT робота для тренировки чувства ритма, Arduino-барабанную установку и EV3-секвенсор. Каждый из них выполнял одну, специфическую функцию, ради которой собственно и создавался.


В проекте EV3 Music Station мы создадим робота-универсала, который сможет стать полноценным участником на ваших музыкальных репетициях. Хотя наш робот и построен на базе LEGO Mindstorms EV3, но использует в своем составе компоненты других наборов - пару моторов от NXT, пару датчиков-кнопок от него же, кроме этого применяются детали LEGO Technic. Традиционно мы подготовили для вас инструкцию по сборке в формате LEGO Digital Designer, которую можно скачать по ссылке.


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

Ритм


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




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


Посмотрите, для примера, как настучал бы человек ритм для двух барабанов (синим цветом - моменты нажатия первой кнопки, красным - второй). Так как нотная запись - точная наука, мы будем привязывать эти нажатия к ближайшим долям такта, выравнивая ритм. Для того, чтобы зациклить ритмический рисунок, последний удар барабана должен повторять первый, это важно. Его мы отбросим, а вот длительность паузы перед ним нам нужна.

Давайте посмотрим на программу ритм-лупера:

' инициализируем датчики-кнопки
Sensor.SetMode(1,0)
Sensor.SetMode(2,0)
' плавно опускаем барабанные палочки в течении 5 секунд
Motor.Start("BC",-3)
Program.Delay(5000)
' останавливаем моторы и сбрасываем энкодеры
Motor.Stop("BС","False")
Motor.ResetCount("BC")
' поднимаем палочки в исходное положение
drum_start = 45
Motor.Move("BC",50,drum_start,"True")

' drum1 и drum2 - логические управляющие сигналы - поднять/опустить палочку
drum1 = "False"
drum2 = "False"
' reg1 и reg2 - верхние положения палочек
reg1 = drum_start 
reg2 = drum_start
' пропорциональный коэф-т регуляторов
k = 0.8

' процедура Drums будет выполняться в параллельном процессе, содержит в себе регуляторы, управляющие положением палочек
Sub Drums
  While "True"
    ' если поступил сигнал поднять/опустить 1-ю палочку - изменяем reg1
    If drum1 = "True" Then
      reg1 = 0
    Else  
      reg1 = drum_start                  
    EndIf
    ' если поступил сигнал поднять/опустить 2-ю палочку - изменяем reg2
    If drum2 = "True" Then
      reg2 = 0
    Else  
      reg2 = drum_start
    EndIf
    
    ' рассчитываем ошибку 1-го регулятора
    e1 = reg1 - Motor.GetCount("B")
    ' рассчитываем управляющее воздействие 1-го регулятора
    v1 = e1 * k
    ' Если палочка поднялась, останавливаем ее
    If Math.Abs(Motor.GetCount("B")) > 40 And drum1 = "False" Then
      Motor.Stop("B","False")
    Else  
    ' иначе подаем управляющее воздействие на мотор
      Motor.Start("B",v1)
    EndIf
    
    ' все тоже самое со вторым регулятором и вторым мотором
    e2 = reg2 - Motor.GetCount("C")
    v2 = e2 * k
    If Math.Abs(Motor.GetCount("C")) > 40 And drum2 = "False" Then
      Motor.Stop("C","False")
    Else     
      Motor.Start("C",v2)  
    EndIf
    
    ' Если палочка опустилась ниже порога, устанавливаем drum1 в "False", тем самым подавая сигнал поднять палочку
    If Motor.GetCount("B") < 20 Then
      drum1 = "False"
    EndIf

    ' тоже самое со вторым мотором, поговое значение отличается, так как банки у нас разные и стучать нужно с разной силой
    If Motor.GetCount("C") < 10 Then
      drum2 = "False"
    EndIf
  EndWhile
EndSub

' запускаем вышеописанную процедуру как параллельный процесс
Thread.Run = Drums  

' создаём переменные
n1 = 0
n2 = 0
' время начала такта
tn = 0
' время конца такта
tc = 0
' время начала "прослушивания" кнопок
d = EV3.Time
n = 0
trigger1 = "False"
trigger2 = "False"

' "прослушиваем" кнопки в течении 8 секунд
While EV3.Time < d + 8000
' если кнопка нажата и не была нажата
  If Sensor.ReadPercent(1) = 100 And trigger1 = "False" Then
  ' если это первое нажатие
    If n = 0 Then
      n = EV3.Time
    EndIf  
    ' запоминаем что кнопка уже нажата
    trigger1 = "True"
    ' в массив n1 заносим время нажатия
    b1[n1] = EV3.Time - n   
    ' переходим к следующему элементу массива
    n1 = n1 + 1
  Else
    ' если кнопка отжата - сбрасываем триггер
    If Sensor.ReadPercent(1) = 0 Then   
      trigger1 = "False"
    EndIf
  EndIf  
  ' повторяем для второй кнопки и массива n2
  If Sensor.ReadPercent(2) = 100 And trigger2 = "False" Then
    If n = 0 Then
      n = EV3.Time
    EndIf    
    trigger2 = "True"
    b2[n2] = EV3.Time - n
    n2 = n2 + 1
  Else
    If Sensor.ReadPercent(2) = 0  Then
      trigger2 = "False"
    EndIf
  EndIf    
EndWhile

' определяем начало и конец такта
If b1[0] < b2[0] Then
  tn = b1[0]  
Else  
  tn = b2[0]
EndIf  
If b1[n1-1] > b2[n2-1] Then
  tc = b1[n1-1]  
Else  
  tc = b2[n2-1]
EndIf  
' определяем длительность такта
dt = tc - tn  

' создаем пустые массивы с 16-ми долями такта для каждого барабана
For r = 0 To 16
  d1[r] = 0
  d2[r] = 0
EndFor  

' привязываем время нажатия кнопок к ближайшим долям такта
For e = 0 To n1 - 1
  d1[Math.Round(b1[e]/dt*16)] = 1
EndFor
For e2 = 0 To n2 - 1
  d2[Math.Round(b2[e2]/dt*16)] = 1
EndFor

' выводим на экран содержимое массивов d1 и d2 для контроля

LCD.Clear()

x = 0
y = 0
For u = 0 To 16
  LCD.Text(1,x,y,1,d1[u])
  x = x + 10
EndFor  
x2 = 0
y2 = 10
For u2 = 0 To 16
  LCD.Text(1,x2,y2,1,d2[u2])
  x2 = x2 + 10
EndFor
' любуемся результатом на экране в течении 10 секунд
Program.Delay(10000)  

' начинаем проигрывать ритм в цикле
While "True"
  For i = 0 To 15
    ' если в ячейке массива d1 единица - отсылаем процессу Drums сигнал drum1 = "True"
    If d1[i] = 1  Then 
      drum1 = "True"
    EndIf
    ' если в ячейке массива d2 единица - отсылаем процессу Drums сигнал drum2 = "True"
    If d2[i] = 1  Then 
      drum2 = "True"    
    EndIf
  ' ждем 1/16 такта
  Program.Delay(dt/16)
  EndFor
EndWhile



Мелодия


Контроллер EV3 обладает довольно развитыми звуковыми возможностями, на нем вполне можно слушать музыку, синтезировать речь. Например в проекте "EV3 Internet Radio Receiver" мы слушали с его помощью онлайн-радио. Однако базовая LEGO-прошивка таких крутых возможностей нам не дает и в данном проекте мы ограничимся использованием одноголосного спикера, который может воспроизводить ноты в пределах нескольких октав.


Записывать последовательность вызовов Speaker.Note в программу мы конечно же не будем, а попробуем использовать ранее не испытанные возможности EV3 Basic по работе с файлами и текстовыми строками.
Для начала определимся с форматом файла. Мы придумали вот такой вот простейший формат хранения (см. ниже).

90
4 4
1
0000000000001000
0000000000000000
Ps  4  Ps  4  Ps  4  A5  4 
Ps  1 
2
1010101010101010
1000100010101000
E5  4  A5  4  E5  4  A5  4 
Ps  1 

Первая строка в файле - bpm - удары в минуту - показатель, определяющий скорость исполнения или воспроизведения композиции. BPM — это количество четвертных нот в минуту, например, 120 BPM означает, что в минуту играется 120 четвертных нот (следовательно, 2 четверти в секунду), или 120 четвертных ударов метронома в минуту.
Вторая строка - размер (4/4, 3/4 и т.п.).
Далее идут такты, первая строка такта содержит его номер.
Вторая строка - удары первого барабана (тарелки) в 16-х долях такта.
Третья строка - удары второго барабана (рабочего) в 16-х долях такта
Четвертая строка содержит 7-символьные текстовые последовательности воспроизводимых нот мелодии - первые три символа - нота с указанием октавы, разделитель, длительность ноты в долях такта. Ps - пауза.
Пятая строка зарезервирована под аккорды гармонии.
Файл завершается номером такта 999.

Чтобы открыть файл для чтения будем использовать следующую конструкцию:

MyFile = EV3File.OpenRead("/home/root/lms2012/prjs/EV3MusicStation/file.txt")

Считать текстовую строку из него можно так

size = EV3File.ReadLine(MyFile)

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

Гармония


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


Пятая, зарезервированная строка в каждом такте может содержать аккорды гармонии

3
1010101010101010
1000100010001000
C5  8  C5  8  C5  8  B4  4  C5  4  Ps 8
C   1

Формат такой же, 7-символьный, тоника аккорда указывается в первом символе, причем "С" - чистое "До", а "с" - До-диез. Следующий два символа - возможная надстройка аккорда, например минор - "m", септаккорды - "7", "m7", пауэр-аккорд - "5". В конце длительность, в течении которой он действует. Ps - все также пауза.

Пример строки гармонии: "Ps  8  Am  4  Dm  4  C   8  Ps  4  "

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



В заключение хотелось бы отметить, что EV3 Basic, в отличии от официального ПО LEGO отлично работает с массивами строк. Это очень важная особенность, существенно расширяющая применимость данного инструментария.

В ходе работы над проектом мы обнаружили интересную особенность - штатная прошивка LEGO с легкостью выполняет функции, которых нет в официальном ПО от LEGO, например функции работы со строками

Text.GetSubText() - вырезать кусок подстроки
Text.GetLength() - вычислить дkину строки

Кроме этого прекрасно работает перевод строки в число с помощью функции EV3File.ConvertToNumber(). Такая возможность была в LEGO NXT-G, а вот в ПО LEGO EV3 ее очень недоставало. Из новых функций, которые мы использовали в проекте также стоит отметить Math.Remainder() - остаток от деления и Math.Round() - округление до ближайшего целого. Примеры применения всех этих функций вы можете посмотреть в исходном коле программы.

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

Самое популярное