Created
October 23, 2016 22:09
-
-
Save cyriux/ff67a25132a695658ea26a4428f804c4 to your computer and use it in GitHub Desktop.
Generative-music bluff code from generic patterns manipulations (clone & transform, recombine, group), with a concept of distance (similarity, expected to be not too high & not too low) between the patterns.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.martraire.generativemusic; | |
import static org.apache.commons.lang3.StringUtils.getLevenshteinDistance; | |
import static org.apache.commons.lang3.StringUtils.join; | |
import java.util.ArrayList; | |
import java.util.Arrays; | |
import java.util.Collections; | |
import java.util.Iterator; | |
import java.util.List; | |
import java.util.stream.Collectors; | |
import java.util.stream.Stream; | |
/** | |
* An ordered sequence of values that may represent the notes, beats or velocity | |
* accents in a melody, timbre changes in a beat, or even the structure of a | |
* song. A pattern is meant to be cloned, mutated, appended to other patterns to | |
* form new patterns. A pattern may also describe how to re-combine other | |
* patterns, and may itself describe a sequence of transformations to be applied | |
* to a pattern. | |
* | |
* A pattern is agnostic to any given musical system. However it is meant to be | |
* translated into notes, beats etc. within a chosen musical system (scales, | |
* rhythms grids etc.). | |
* | |
* For example, we may have: "1 2 2 3 = D Eb Eb F", | |
* "1 0 0 1 0 0 1 0 0 0 1 = x..x..x....x", or | |
* "1 2 1 3 1 2 1 4 1 = kick snare kick clap kick snare kick power-snare", or | |
* "0 2 1 3 1 1 4 1 1 5 = intro verse chorus verse 2 chorus chorus bridge chorus chorus outro" | |
* | |
* | |
* Generative music may be created in a few passes: 1/ generate a few base | |
* patterns, plus some transformation-combination-grouping patterns 2/ transform | |
* each of the patterns according to transformation patterns, resulting into | |
* more patterns 3/ combine all the resulting patterns according to the | |
* combination patterns 4/ group some of these patterns into groups into another | |
* pool, according to the grouping patterns. | |
* | |
* Then translate these groups through a given musical system. For examples, | |
* each group may correspond to a track where the first pattern describes notes | |
* in a scale, and the second pattern describes the beat on the grid. One | |
* special group may describe the song arrangement. | |
*/ | |
public class Pattern implements Iterable<Integer> { | |
private final List<Integer> values; | |
public Pattern(List<Integer> values) { | |
this.values = values; | |
} | |
public Pattern(Integer... values) { | |
this(Arrays.asList(values)); | |
} | |
public Pattern(Stream<Integer> stream) { | |
this(stream.collect(Collectors.toList())); | |
} | |
@Override | |
public Iterator<Integer> iterator() { | |
return values.iterator(); | |
} | |
public Pattern mutate(MutationType mutationType, int times) { | |
Pattern mutated = this; | |
while (times-- > 0) { | |
mutated = mutated.mutate(mutationType); | |
} | |
return mutated; | |
} | |
/** | |
* Transforms the given pattern according to this pattern acting as the | |
* recipe for the sequence of successive mutations (mutate), where each | |
* value corresponds to a type of mutation to apply. | |
*/ | |
public Pattern Transform(Pattern pattern) { | |
final List<MutationType> mutations = MutationType.fromIntegerSequence(values); | |
Pattern mutated = pattern; | |
for (MutationType mutationType : mutations) { | |
mutated = mutated.mutate(mutationType); | |
} | |
return mutated; | |
} | |
public Pattern combine(Pattern... patterns) { | |
return combine(Arrays.asList(patterns)); | |
} | |
/** | |
* Combines some of the given patterns together according to this pattern | |
* acting as the recipe for the successive recombination (append), where | |
* each value identifies the pattern to append by its index in the given | |
* list. | |
*/ | |
public Pattern combine(List<Pattern> patterns) { | |
Pattern combined = new Pattern(); | |
final int n = patterns.size(); | |
if (n == 0) { | |
return combined; | |
} | |
for (int i : this) { | |
combined = combined.append(patterns.get(i % n)); | |
} | |
return combined; | |
} | |
/** | |
* Groups some of of the given patterns together according to this pattern | |
* acting as the recipe for the grouping (add to the returned list), where | |
* each value identifies the pattern to include by its index in the given | |
* list. | |
*/ | |
public List<Pattern> group(List<Pattern> patterns) { | |
List<Pattern> group = new ArrayList<>(); | |
final int n = patterns.size(); | |
if (n == 0) { | |
return group; | |
} | |
for (int i : this) { | |
group.add(patterns.get(i % n)); | |
} | |
return group; | |
} | |
public Pattern mutate(MutationType mutationType) { | |
switch (mutationType) { | |
case NONE: | |
return this; | |
case REVERSE: | |
return reverse(); | |
case ROLL: | |
return roll(); | |
case SHIFT_UP: | |
return shift(1); | |
case SHIFT_DOWN: | |
return shift(-1); | |
case STRETCH: | |
return stretch(); | |
case SHORTEN: | |
return shorten(); | |
case TRUNCATE: | |
return truncate(); | |
} | |
return this; | |
} | |
private ArrayList<Integer> toNewList() { | |
return new ArrayList<Integer>(values); | |
} | |
public Pattern reverse() { | |
final ArrayList<Integer> list = toNewList(); | |
Collections.reverse(list); | |
return new Pattern(list); | |
} | |
public Pattern shift(int offset) { | |
return new Pattern(values.stream().map(i -> i + offset)); | |
} | |
public Pattern roll() { | |
if (values.size() < 2) { | |
return this; | |
} | |
final ArrayList<Integer> list = toNewList(); | |
Integer first = list.remove(0); | |
list.add(first); | |
return new Pattern(list); | |
} | |
public Pattern stretch() { | |
final ArrayList<Integer> list = new ArrayList<>(); | |
for (Integer i : values) { | |
list.add(i); | |
list.add(i); | |
} | |
return new Pattern(list); | |
} | |
public Pattern shorten() { | |
final ArrayList<Integer> list = new ArrayList<>(); | |
int index = 0; | |
for (Integer i : values) { | |
if (index++ % 2 == 0) { | |
list.add(i); | |
} | |
} | |
return new Pattern(list); | |
} | |
public Pattern truncate() { | |
if (values.isEmpty()) { | |
return this; | |
} | |
final ArrayList<Integer> list = toNewList(); | |
list.remove(list.size() - 1); | |
return new Pattern(list); | |
} | |
public Pattern append(Pattern t) { | |
final ArrayList<Integer> list = toNewList(); | |
list.addAll(t.values); | |
return new Pattern(list); | |
} | |
public int distanceTo(Pattern other) { | |
if (equals(other)) { | |
return 0; | |
} | |
return getLevenshteinDistance(asString(), other.asString()); | |
} | |
private String asString() { | |
return join(values, ""); | |
} | |
@Override | |
public int hashCode() { | |
return values.hashCode(); | |
} | |
@Override | |
public boolean equals(Object obj) { | |
if (this == obj) { | |
return true; | |
} | |
if (!(obj instanceof Pattern)) { | |
return false; | |
} | |
final Pattern other = (Pattern) obj; | |
return values.equals(other.values); | |
} | |
@Override | |
public String toString() { | |
return values.toString(); | |
} | |
public static enum MutationType { | |
NONE, REVERSE, TRUNCATE, ROLL, SHIFT_UP, SHIFT_DOWN, STRETCH, SHORTEN; | |
public final static MutationType fromInteger(int index) { | |
final MutationType[] values = MutationType.values(); | |
return values[index % values.length]; | |
} | |
public final static List<MutationType> fromIntegerSequence(final List<Integer> indexes) { | |
return indexes.stream().map(i -> MutationType.fromInteger(i)).collect(Collectors.toList()); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment