Когда у вас начнёт достаточно хорошо получаться лайвкодинг с большим количеством потоков, выполняющихся одновременно, вы, скорее всего, заметите, что очень легко допустить ошибку, которая может прервать поток. Ничего страшного в этом нет, так как поток может быть перезапущен простым нажатием кнопки “Выполнить”. Однако, после перезапуска потока он будет играть невпопад с другими.
Как мы обсуждали ранее, новые потоки, созданные при помощи in_thread наследуют все настройки от родительского потока. Они включают и текущую метку времени. Это означает, что потоки всегда синхронизированы друг с другом после одновременного их запуска.
Но, когда поток стартует отдельно от родительского, внутри него ведётся собственный отсчет времени, который вряд ли совпадёт с каким-либо из других активных потоков.
В Sonic Pi решением этой проблемы являются функции cue и sync.
cue позволяет нам отправлять сигнал пульса другим потокам. По умолчанию, другие потоки не слушают и пропускают эти сообщения. Однако, поток может легко заявить о своей заинтересованности, вызвав функцию sync.
Важно осознавать, что функция sync похожа на sleep, ведь она останавливает текущий поток, и тот некоторое время не выполняется. Для sleep время простоя указывается явно, но вызвав sync, вы не знаете сколько времени оно продлится, потому что sync ждёт следующий cue от другого потока. Это может случиться скоро, а может и нет.
Давайте выясним немного больше деталей:
in_thread do
loop do
cue :tick
sleep 1
end
end
in_thread do
loop do
sync :tick
sample :drum_heavy_kick
end
end
Здесь у нас есть два потока. Один работает как метроном. Он не играет никаких звуков, только отправляет :tick сигнал каждую долю такта. Второй поток синхронизуется с сообщениями tick. Когда он получает :tick сообщение, то наследует временную отметку потока, вызвавшего cue, и продолжает выполнение.
В результате мы будем слышать сэмпл :drum_heavy_kick в тот самый момент, когда другой поток отправляет :tick сигнал. Даже если два этих потока стартовали не одновременно, это всё равно будет происходить:
in_thread do
loop do
cue :tick
sleep 1
end
end
sleep(0.3)
in_thread do
loop do
sync :tick
sample :drum_heavy_kick
end
end
Результатом вызова sleep будет расхождение второго потока с первым. Однако, так как мы используем cue и sync, то мы автоматически синхронизируем потоки и избегаем любых случайных временных сдвигов.
Для сигналов cue можно использовать любые названия, а не только :tick. Просто следите за тем, чтобы все остальные потоки вызывали sync с правильным именем. Иначе они остановятся навсегда (или, по крайней мере, пока вы не нажмёте кнопку “Остановить”).
Попробуем запрограммировать что-нибудь с использованием нескольких имен cue:
in_thread do
loop do
cue [:foo, :bar, :baz].choose
sleep 0.5
end
end
in_thread do
loop do
sync :foo
sample :elec_beep
end
end
in_thread do
loop do
sync :bar
sample :elec_flip
end
end
in_thread do
loop do
sync :baz
sample :elec_blup
end
end
Тут у нас есть цикл с функцией cue, отправляющей пульс. Случайным образом этот пульс может быть назван :foo, :bar или :baz. Ещё есть три цикла в потоках, которые синхронизируются с каждым из этих сигналов независимо и воспроизводят разные сэмплы. Чистый эффект от этого кода в том, что каждые полсекунды мы слышим звук, так как каждый из потоков sync случайным образом синхронизируется с потоком cue и проигрывает свой сэмпл.
Конечно же, код будет работать даже если расположить потоки в обратном порядке, поскольку потоки будут дожидаться следующего cue.