В нашем новом проекте мы вновь коснемся музыкальной темы. Ранее мы уже строили несколько музыкальных "роботов": NXT-гитару, EV3-гитару, EV3-скрипку, NXT робота для тренировки чувства ритма, Arduino-барабанную установку и EV3-секвенсор. Каждый из них выполнял одну, специфическую функцию, ради которой собственно и создавался.
В проекте EV3 Music Station мы создадим робота-универсала, который сможет стать полноценным участником на ваших музыкальных репетициях. Хотя наш робот и построен на базе LEGO Mindstorms EV3, но использует в своем составе компоненты других наборов - пару моторов от NXT, пару датчиков-кнопок от него же, кроме этого применяются детали LEGO Technic. Традиционно мы подготовили для вас инструкцию по сборке в формате LEGO Digital Designer, которую можно скачать по ссылке.
Если у вас нет каких-то деталей - не беда, проявите фантазию и замените их имеющимися - так строить гораздо интереснее.
Программировать робота мы будем на EV3 Basic, к которому уже успели привыкнуть. В этом проекте мы освоим в нем новые функции - работу с файлами и текстовыми строками.
Как гласит музыкальная теория, музыка состоит из мелодии, гармонии и ритма, которые в свою очередь состоят из звуков и звукосочетаний различной высоты и длительности и образуют внешнюю форму музыкального произведения.
В проекте 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 четвертных ударов метронома в минуту.
Пятая строка зарезервирована под аккорды гармонии.
Файл завершается номером такта 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() - округление до ближайшего целого. Примеры применения всех этих функций вы можете посмотреть в исходном коле программы.
Чтение и обработка текста из файл оказалась довольно неторопливой, что не позволяла использовать их в реальном времени (между тактами образовывались неприятные паузы), поэтому мы сначала считываем композицию из текстового файла в массивы с ритмом, мелодией и гармонией, а затем воспроизводим композицию используя эти структуры данных.
Первый режим, в котором сможет работать наш робот - режим ритм-лупера. Настучав по паре кнопок нужный нам ритм, мы предлагаем роботу его повторить, зациклив таким образом, чтобы он представлял собой законченный ритмический рисунок. Человек - существо аналоговое, настучать, да еще и по кнопкам ровно сможет далеко не каждый, поэтому мы будем программно выравнивать удары по барабанам, привязываясь к ближайшим долям такта.
Посмотрите, для примера, как настучал бы человек ритм для двух барабанов (синим цветом - моменты нажатия первой кнопки, красным - второй). Так как нотная запись - точная наука, мы будем привязывать эти нажатия к ближайшим долям такта, выравнивая ритм. Для того, чтобы зациклить ритмический рисунок, последний удар барабана должен повторять первый, это важно. Его мы отбросим, а вот длительность паузы перед ним нам нужна.
' инициализируем датчики-кнопки
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
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() - округление до ближайшего целого. Примеры применения всех этих функций вы можете посмотреть в исходном коле программы.
Чтение и обработка текста из файл оказалась довольно неторопливой, что не позволяла использовать их в реальном времени (между тактами образовывались неприятные паузы), поэтому мы сначала считываем композицию из текстового файла в массивы с ритмом, мелодией и гармонией, а затем воспроизводим композицию используя эти структуры данных.