Когда у вас начнёт достаточно хорошо получаться лайвкодинг с большим количеством потоков, выполняющихся одновременно, вы, скорее всего, заметите, что очень легко допустить ошибку, которая может прервать поток. Ничего страшного в этом нет, так как поток может быть перезапущен простым нажатием кнопки “Выполнить”. Однако, после перезапуска потока он будет играть невпопад с другими.
Как мы обсуждали ранее, новые потоки, созданные при помощи 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
.