The post has been translated automatically. Original language: Russian
You can write a program that randomly selects pentatonic notes and puts them on a time grid. It will indeed create musical sequences, but randomness alone guarantees almost nothing.
It is more interesting to build a system in a different way: first determine the acceptable elements and rules, then allow the program to create variants only within these boundaries, and then check the result separately. Then generation and verification become two different parts of the same system.
Let's start small
Let's say we have five notes of the pentatonic:
A C D E G
This is already enough to get a large number of sequences. But if you just pick a random note at regular intervals, the music will quickly become monotonous.
So let's add a rhythm. Let's split the measure into eight positions and allow each position to either start a note or leave a pause.
Now the program selects not only the pitch of the sound, but also the moment of its appearance.
Even such a simple model creates a large space of options.
The generator needs restrictions
Complete randomness rarely gives an interesting result. Therefore, we will set some rules.
For example, adjacent notes should not be too often far apart. After several notes in a row, it is advisable to pause. A short motif can sometimes be repeated by slightly changing the ending.
In addition, steady notes can be used more often on strong parts. This is not an absolute musical law, but just a rule of our particular model.
This results in an important transition: the program no longer chooses from all possible options in the same way. She works inside the space that we have pre-formed.
Generation rules and validation rules are not the same thing.
There is a subtle difference here.
If the generator is told to use only five notes, this does not mean that we do not need a separate check of this property. The generator may contain an error. It can be changed later. One rule may accidentally break another.
Therefore, it is useful to separate the two parts of the system.
The first part creates the result:
generate() → phrase
The second one checks it:
verify(phrase) → true / false
Such a division seems redundant only in a small example. In a large program, it becomes a way to avoid confusing intention and proof of compliance.
What can be checked
Several precise properties can be formulated for our blues phrase.
Each note must belong to a given pentatonic. The beginning of the note must be at the allowed position on the time grid. The total duration of the events should not exceed the limits of the phrase.
You can add range restrictions so that the melody doesn't jump all over the fingerboard. You can check that there is no forbidden jump between adjacent notes or that at least one event is present in each measure.
All these properties are easy to formulate unambiguously. This means that the program is able to check them without human intervention.
The simplest scheme
Conditionally, the program may look like this:
phrase = generate()
assert notes_are_allowed(phrase)
assert rhythm_is_valid(phrase)
assert range_is_valid(phrase)
assert phrase_has_correct_length(phrase)
play(phrase)This is not yet a formal proof of the entire program, but the architecture itself is already important. The result is not sent on immediately after creation.
A property check appears between generation and use.
This principle is found far beyond music. The system creates data, a state, or a solution, and then checks whether the result meets the required conditions.
But "right" does not mean "beautiful" yet
Let's assume that all the checks have been successfully completed. Each note is allowed, the rhythm is acceptable, the range is respected, the length of the phrase is correct.
The phrase may still sound boring.
This is not a lack of validation. This is a consequence of the fact that we checked exactly those properties that we were able to formally determine.
The program proved that the result is acceptable. She didn't prove it: the result is musically interesting.
This creates an important distinction between correctness and quality.
Correctness is usually easier to express with strict rules. Quality often depends on context, experience, taste, and a large number of weak features.
You can gradually formalize more
Let's say we notice that the program repeats one note too often. Then you can add a new rule: the same note should not be repeated more than three times in a row.
Then it turns out that the phrases have no development. We introduce the concept of a short motif and require that part of the motif be repeated at least once with a change.
Then it turns out that the pauses are poorly distributed. We are adding restrictions to them as well.
The system begins to accumulate knowledge about what we consider to be a good structure.
This is how software is often developed. First, a behavior occurs, then an undesirable case is discovered, after which a new rule or a new invariant appears.
If the rule can be formulated precisely, it can be checked automatically.
The error becomes useful
Imagine that the generator has created such a sequence:
A C D F G
The F note is not included in the given pentatonic. The check immediately detects a violation.
In the simplest version, the program will simply discard this phrase and create a new one.
But it can be made more useful. The system can save the fact of the violation itself: which rule was not fulfilled, which element caused it, and at what stage it appeared.
Then the error becomes not just a failure, but information about the behavior of the system.
This is already an important engineering principle: a good check should not only say "wrong", but also help to understand which property has been violated.
The stricter the system, the more important the boundaries of the rules are.
You can try to ban all suspicious cases altogether. Then the program will create only very predictable phrases.
The music will quickly lose its character.
Therefore, the task is not to maximize the number of restrictions. We need to find a minimum set of rules that separates the truly unacceptable from the space of free behavior.
The same thing happens with programs. A good specification does not describe every little detail of the implementation. It captures properties that are really dangerous to violate.
Rules that are too weak don't guarantee anything. Too rigid does not leave the system any freedom.
Generation and provable boundaries
Now you can look at the whole structure.
There is a small set of elements. There are rules for combining them. There is a generator that can create many options. There is a property check that separates an acceptable result from an unacceptable one.
The music here is convenient because the result can be heard immediately. If the program is playing strangely, the person notices it almost instantly and starts looking for which rule is missing.
In more complex systems, the error can be hidden for much longer. Therefore, it is especially important to determine in advance the properties that should not be violated.
Five notes, a rhythmic grid, and a few rules look like a toy example. But the scheme itself is quite serious.:
elements → rules → generation → validation → result
It combines music, mathematics and programming. Music provides a space of options, mathematics provides precise properties, and the program turns it all into a working system.
And perhaps the main result is not that the computer has learned to play simple blues. Something else is much more interesting: we forced him not only to create, but also to test the boundaries of his own behavior.
Можно написать программу, которая случайно выбирает ноты пентатоники и ставит их на временную сетку. Она действительно будет создавать музыкальные последовательности, но сама по себе случайность почти ничего не гарантирует.
Интереснее построить систему иначе: сначала определить допустимые элементы и правила, затем позволить программе создавать варианты только внутри этих границ, а после этого отдельно проверить результат. Тогда генерация и проверка становятся двумя разными частями одной системы.
Начнём с малого
Пусть у нас есть пять нот пентатоники:
A C D E G
Этого уже достаточно, чтобы получить большое число последовательностей. Но если просто выбирать случайную ноту через одинаковые промежутки времени, музыка быстро станет монотонной.
Поэтому добавим ритм. Разобьём такт на восемь позиций и разрешим в каждой позиции либо начать ноту, либо оставить паузу.
Теперь программа выбирает уже не только высоту звука, но и момент его появления.
Даже такая простая модель создаёт большое пространство вариантов.
Генератору нужны ограничения
Полная случайность редко даёт интересный результат. Поэтому зададим несколько правил.
Например, соседние ноты не должны слишком часто находиться далеко друг от друга. После нескольких нот подряд желательно сделать паузу. Короткий мотив иногда можно повторить, слегка изменив окончание.
Кроме того, на сильных долях можно чаще использовать устойчивые ноты. Это не абсолютный музыкальный закон, а просто правило нашей конкретной модели.
Получается важный переход: программа больше не выбирает из всех возможных вариантов одинаково. Она работает внутри пространства, которое мы заранее сформировали.
Правила генерации и правила проверки — не одно и то же
Здесь возникает тонкое различие.
Если генератору сказано использовать только пять нот, это ещё не означает, что нам не нужна отдельная проверка этого свойства. Генератор может содержать ошибку. Его могут изменить позже. Одно правило может случайно нарушить другое.
Поэтому полезно разделять две части системы.
Первая часть создаёт результат:
generate() → phrase
Вторая проверяет его:
verify(phrase) → true / false
Такое разделение кажется избыточным только в маленьком примере. В большой программе оно становится способом не смешивать намерение и доказательство его соблюдения.
Что можно проверить
Для нашей блюзовой фразы можно сформулировать несколько точных свойств.
Каждая нота должна принадлежать заданной пентатонике. Начало ноты должно находиться на разрешённой позиции временной сетки. Суммарная длительность событий не должна выходить за пределы фразы.
Можно добавить ограничения диапазона, чтобы мелодия не прыгала по всему грифу. Можно проверить, что между соседними нотами нет запрещённого скачка или что в каждом такте присутствует хотя бы одно событие.
Все эти свойства легко сформулировать однозначно. Значит, программа способна проверять их без человеческого участия.
Простейшая схема
Условно программа может выглядеть так:
phrase = generate()
assert notes_are_allowed(phrase)
assert rhythm_is_valid(phrase)
assert range_is_valid(phrase)
assert phrase_has_correct_length(phrase)
play(phrase)Это ещё не формальное доказательство всей программы, но сама архитектура уже важна. Результат не отправляется дальше сразу после создания.
Между генерацией и использованием появляется проверка свойств.
Такой принцип встречается далеко за пределами музыки. Система создаёт данные, состояние или решение, после чего проверяет, удовлетворяет ли результат обязательным условиям.
Но «правильно» ещё не означает «красиво»
Предположим, все проверки успешно пройдены. Каждая нота разрешена, ритм допустим, диапазон соблюдён, длина фразы правильная.
Фраза всё равно может звучать скучно.
Это не недостаток проверки. Это следствие того, что мы проверили именно те свойства, которые сумели формально определить.
Программа доказала: результат допустим. Она не доказала: результат музыкально интересен.
Так появляется важное различие между корректностью и качеством.
Корректность обычно проще выразить строгими правилами. Качество часто зависит от контекста, опыта, вкуса и большого числа слабых признаков.
Можно постепенно формализовать больше
Допустим, мы замечаем, что программа слишком часто повторяет одну ноту. Тогда можно добавить новое правило: одна и та же нота не должна повторяться больше трёх раз подряд.
Потом выясняется, что фразы не имеют развития. Мы вводим понятие короткого мотива и требуем, чтобы часть мотива хотя бы один раз повторялась с изменением.
Затем оказывается, что паузы распределены плохо. Добавляем ограничения и на них.
Система начинает накапливать знания о том, что мы считаем хорошей структурой.
Именно так часто развивается программное обеспечение. Сначала возникает поведение, затем обнаруживается нежелательный случай, после чего появляется новое правило или новый инвариант.
Если правило можно сформулировать точно, его можно проверить автоматически.
Ошибка становится полезной
Представим, что генератор создал такую последовательность:
A C D F G
Нота F не входит в заданную нами пентатонику. Проверка сразу обнаруживает нарушение.
В простейшем варианте программа просто отбросит эту фразу и создаст новую.
Но можно сделать полезнее. Система может сохранить сам факт нарушения: какое правило не выполнено, какой элемент стал причиной и на каком этапе он появился.
Тогда ошибка становится не просто отказом, а информацией о поведении системы.
Это уже важный инженерный принцип: хорошая проверка должна не только говорить «неверно», но и помогать понять, какое свойство было нарушено.
Чем строже система, тем важнее границы правил
Можно попытаться запретить вообще все подозрительные случаи. Тогда программа будет создавать только очень предсказуемые фразы.
Музыка быстро потеряет характер.
Поэтому задача состоит не в максимальном количестве ограничений. Нужно найти минимальный набор правил, который отделяет действительно недопустимое от пространства свободного поведения.
С программами происходит то же самое. Хорошая спецификация не описывает каждую мелочь реализации. Она фиксирует свойства, нарушение которых действительно опасно.
Слишком слабые правила ничего не гарантируют. Слишком жёсткие не оставляют системе свободы.
Генерация и доказуемые границы
Теперь можно посмотреть на всю конструкцию целиком.
Есть небольшой набор элементов. Есть правила их сочетания. Есть генератор, способный создавать множество вариантов. Есть проверка свойств, которая отделяет допустимый результат от недопустимого.
Музыка здесь удобна тем, что результат можно сразу услышать. Если программа играет странно, человек замечает это почти мгновенно и начинает искать, какого правила не хватает.
В более сложных системах ошибка может быть скрыта гораздо дольше. Поэтому там особенно важно заранее определить свойства, которые не должны нарушаться.
Пять нот, ритмическая сетка и несколько правил выглядят как игрушечный пример. Но сама схема вполне серьёзная:
элементы → правила → генерация → проверка → результат
В ней соединяются музыка, математика и программирование. Музыка даёт пространство вариантов, математика — точные свойства, а программа превращает всё это в работающую систему.
И, возможно, главный результат находится не в том, что компьютер научился играть простой блюз. Гораздо интереснее другое: мы заставили его не только создавать, но и проверять границы собственного поведения.