Procedural Music Generation

Started by
22 comments, last by Patient_0 12 years, 7 months ago

Best thing I can recommend (there is rather a derth of good ABC players) is to goto: http://sites.google.com/site/lotroabc/ and get the ABC Navigator- Editor/Player

you may need to reduce the number of notes per line in order to get my example to work, here is a shortened version i truncated a bit to make it work.




Just downloaded it. There were some errors importing the song (I don't think it likes the first [e,.....]).

The tune starts... and then gets surprisingly good!! Very impressive.


Will
Advertisement

[quote name='Net Gnome' timestamp='1312933067' post='4846956']
Best thing I can recommend (there is rather a derth of good ABC players) is to goto: http://sites.google.com/site/lotroabc/ and get the ABC Navigator- Editor/Player

you may need to reduce the number of notes per line in order to get my example to work, here is a shortened version i truncated a bit to make it work.




Just downloaded it. There were some errors importing the song (I don't think it likes the first [e,.....]).

The tune starts... and then gets surprisingly good!! Very impressive.


Will
[/quote]

yea, i ran into a similar issue, but you can make it attempt to play it anyway if you click the name of the song in the central grey area after you load it... it seems to play ok after that even without the truncation from my previous entry.

The best ABC player by far is LoTRO itself. The devs whom designed that have done the best version of an ABC player I've seen. If you have LoTRO, load it up and play the files for a -much- better experience. Even with the 30s limit, i'll try posting a Fraps grab.
Here is the first 30s of the generated song:

Here is the first 30s of the generated song: http://www.youtube.c...h?v=q8m73PKdYik



Well done!! Fantastic! Documented from start to finish-- you deserve some sort of GameDev poster of the month award. :D

I've used markov chains before to build a chatbot, using scripts from movies as the input. The results, while proper english, were less than stellar. Longer scripts got more attention, so the conversation would tend to get stuck in one movie ('How to get ahead in advertising' seemed the computers favorite).

Have you tried importing multiple songs in to a single markov chain? You could get more variety that way.

[quote name='Net Gnome' timestamp='1312966414' post='4847079']
Here is the first 30s of the generated song: http://www.youtube.c...h?v=q8m73PKdYik



Well done!! Fantastic! Documented from start to finish-- you deserve some sort of GameDev poster of the month award. :D

I've used markov chains before to build a chatbot, using scripts from movies as the input. The results, while proper english, were less than stellar. Longer scripts got more attention, so the conversation would tend to get stuck in one movie ('How to get ahead in advertising' seemed the computers favorite).

Have you tried importing multiple songs in to a single markov chain? You could get more variety that way.
[/quote]

yep, just posted a vid with 5 samples instead of one. More variety, but it muddles itself a few times... the next phase may help smooth out those hiccups.




Anyway, havent completed the next code set yet, but i wanted to post a bit on the conceptual thought behind the next iteration.

Currently the tokens are setup in a 1:n associative relationship inside a token dictionary so if you had a 1:3 associative relationship, the first 3 token entries may look like this:

[T1][T2 T3 T4]
[T2][T3 T4 T5]
[T3][T4 T5 T6]

By increasing or decreasing the token association ratio you can control aspects of the output. Essentially how well the music flows from one note to the next, because it builds based on the parsed note history and selects via weighted distribution (because of allowed duplicate entries). However, with larger token ratios, the music becomes more like the original, because there are less probable variations stored in the token dictionary and if the token ratio is small, the music becomes more random due to too many possibilities. What is needed, is a way to select or weight the choice further towards a "desired" distribution, but maintain a randomized selection process to allow for variation. Tiered token associations may resolve this.

Tiered Token associations is almost exatly what it sounds like. Multiple varying associative ratio token dictionaries, such as 1:5, 1:4, 1:3, and 1:2 fit together in a hierarchial fashion from largest ratio (1:5) to lowest (1:2) with the higher tiers weighting allowable distributions on the next lower in the hierarchy. Since the larger ratios capture more realistic music patters, they are more suited for a "strategic" or "thematic" note selection that becomes more "tactical" or "fleeting" as you descend the hierarchy.

ex) 3-tier heirarchy

tier 3 seed chosen: [t2]
[t2] associates to [t2 t3 t4 t1],[t2 t1 t3 t4],[t1 t2 t3 t4]
randomly select theme to be [t1 t2 t3 t4]

tier 2 seed chosen: [t1]
[t1] associates to [t2 t3 t4],[t3 t4 t2],[t2 t4 t3],[t3 t2 t4], [t4, t3, t2]
since tier 3 was [t1 t2 t3 t4], we want to end in a [t2] token so we downselect to [t3 t4 t2],[t4, t3, t2]
randomly select theme to be [t4, t3, t2]

tier 1 seed inital becomes [t4]
[t4] associates to [t2 t3],[t3 t2],[t3 t4],[t1 t3]
since tier 2 was [t4, t3, t2] we want to end in a [t3], so we downselect to [t2 t3],[t1 t3]
randomly select [t1 t3] as the note run as downselecting 1:1 ratio will produce the same result (unless you store additional note information in your tokens like octaves and allow for octave variation thresholds).

You would continue to recurse through the hierarchy until you reach you final desired tier 3 token count.

What if a run association isnt found? Then you build on your current token tier until you encounter a matching end token that completes the theme selection and then move on from there.

So that is the basis I'm thinking of, and it should produce a better result than my last video, but how much better remains to be seen. Hopefully I can get this coded this weekend, but we'll see.





What if a run association isnt found? Then you build on your current token tier until you encounter a matching end token that completes the theme selection and then move on from there.

So that is the basis I'm thinking of, and it should produce a better result than my last video, but how much better remains to be seen. Hopefully I can get this coded this weekend, but we'll see.


I think you're at the point where the markov-chain isn't going to be enough on its own. I'm not a musician, but I would think you'll need to check the 'sanity' of the following:

1. Does the addition of sequence [t2 t3 t4 ... tN] stay in key?
2. Does the addition cause the sequence to lose tempo?
3. Does the sequence belong to the apporpriate section of the song? (most songs have middles, endings, etc..)

So, rather than just randomly pick from the chain, you need to pick, check, and if failed, pick a new one.

Something that would be really nice would be multi-voice compositions. So a melody voice and a bass voice for example. Basically each value in the markov chain would contain 2 values-- one for each voice. Could be tricky due to tempo/timing though...
The concept worked! This version produced the best ones yet. Some of the ones it comes up with are good enough to use as decent game music.

Anyway, here are two vids I put together showing some of the results:

A slow tune:
http://www.youtube.c...h?v=YJe05wQglDw

A fast tune:
http://www.youtube.c...h?v=mohqgYDZL_g

So you can see (and hear) that the tiered concept worked very well.

Here is one of the songs, the slow one i think (i overwrote the fast one =/ )


X:1
T:PROCEDURAL GENERATED TUNE
L:1/4
Q:90
K:C
[G7/8z/4] [C3/4z3/8] F3/8 [G,23/8z3/4] [D11/8z3/8][A9/8z3/8] F/4 D3/8 [^D,3/2z3/4] [^A,11/8z3/8]c'3/8 [C11/8z/4] [F3/2^a/2z3/8] [F,3/2z3/8] [G=a/2z3/8][c'3/8z/4] [C3/2z3/8] [F3/2^a/2z3/8] [F,11/8z3/8] [G15/8=a/2z3/8][G15/8=a/2z3/8] [C5/8z3/8] F/4 [C,23/8c'z3/4] [C11/8z3/8][G7/8=a/2z3/8] [C5/8z/4] F3/8 [G,23/8a3/4] [D11/8g3/8][G7/8=a3/8z/4] [C3/4z3/8] F3/8 [G,23/8a3/4] [D11/8g3/8]A3/8 F3/8 D/4 [^D,3/2z3/4] [^A,11/8z3/8][c'/2z3/8] [C11/8z3/8] [F11/8^a3/8z/4] [F,3/2z3/8] [G=a/2z3/8][Gz3/8] [C5/8z3/8] F/4 [=D,3/2dz3/4] [C3/2z3/8][G7/8z3/8] [C5/8z/4] F3/8 [G,23/8dz3/4] [Fz3/8][G7/8z3/8] [C5/8z/4] F3/8 [=D,3/2dz3/4] [C11/8z3/8]A3/8 F3/8 D/4 [^D,3/2z3/4] [^A,11/8z3/8]A/4 F3/8 D3/8 [^D,11/8z3/4] [^A,11/8z3/8][Gz3/8] [C5/8z3/8] F/4 [G,2z3/8] [D3/2z3/8][G7/8=a/2z3/8] [C5/8z3/8] F/4 [G,23/8a3/4] [D11/8g3/8][G15/8=a3/8] [C5/8z/4] F3/8 [C,23/8c'z3/4] [C11/8z3/8][G7/8=a3/8z/4] [C3/4z3/8] F3/8 [G,23/8a3/4] [D11/8g3/8][G7/8z3/8] [C5/8=a5/8z3/8] F/4 [C,23/8g11/2z3/8] [C15/8z3/8][C15/8z3/8] [F11/8z/4] [^A3/4z3/8] [F,3/2z3/8] [=Az3/8][F3/4z3/8] [^A7/8z3/8] [G7/8z/4] [F,3/2z3/8] [c2z3/8]G/4 [=Az3/8] [F3/2z3/8] [F,3/2z3/8] c3/8[F11/8z3/8] [F,11/8z3/8] [G7/8z/4] [C3/4z3/8] F3/8[F11/8z3/8] [F,11/8z/4] [Gz3/8] [C3/4z3/8] F3/8[F11/8z3/8] [F,11/8z/4] [G3/2z3/8] [C3/2z3/8] [F/2z3/8][F11/8z/4] [F,3/2z3/8] [Gz3/8] [C3/4z3/8] F3/8[^D11/8gz3/4] [G11/8z/4] [C3/4=d2z3/8] ^D3/8 [=D,3/2z3/8][F11/8z3/8] [F,11/8c'5/8z3/8] [G7/8z/4] [C3/4^a3/8] [F3/8=a3][F11/8^a/2z3/8] [F,11/8z3/8] [G7/8=a3/8z/4] [C3/4z3/8] F3/8[F11/8=d3/8] [F,11/8z/4] [G2c'/2z3/8] [C3/4z3/8] F3/8[F11/8^a3/8z/4] [F,3/2z3/8] [G=a/2z3/8] [C3/4z3/8] F3/8[F11/8a7/8z3/8] [D,11/8z/4] [Gz3/8] [C3/4a3/4z3/8] F3/8[F11/8z3/8] [F,11/8z/4] [Gz3/8] [C3/4z3/8] F3/8[F11/8z/4] [F,3/2z3/8] [Gz3/8] [C3/4z3/8] F3/8[^D11/8g7/8z5/8] [G3/2z3/8] [C3/4=d15/8z3/8] ^D3/8 [=D,11/8z3/8][F11/8^a/2z3/8] [F,11/8z3/8] [G7/8=a3/8z/4] [C3/4z3/8] F3/8[F11/8z3/8] [F,11/8c'5/8z/4] [Gz3/8] [C3/4^a3/8] [F3/8=a3][F11/8f11/8z/4] [F,3/2z3/8] [Gz3/8] [C3/4z3/8] F3/8[F11/8z3/8] [F,11/8z3/8] [G7/8z/4] [C3/4z3/8] F3/8[F3/2z3/8] [^D,11/8z3/8] [G15/8z3/8] [^A,5/8z3/8] [F11/8z/4][F11/8z3/8] [^D,11/8z3/8] [G15/8z3/8] [^A,5/8z/4] [F3/2z3/8][F3/2z3/8] [F,3/2z3/4] [C11/8z3/8] [F11/8z3/8] [F,11/8z/4][F3/2z3/8] [^D,11/8^a3/8] [G15/8c'3/8] [^A,5/8d3/8] [F11/8c'11/8z/4][=F11/8f3/8] [^D,11/8d3/8] [G15/8^d5/8z3/8] [^A,5/8z/4] [F3/2f/2z3/8][F3/2d3/2z3/8] [^D,11/8z3/8] [G15/8z3/8] [^A,5/8z3/8] [F11/8z/4][F11/8^az3/8] [^D,11/8z3/8] [G15/8z3/8] [^A,5/8d/4] [F3/2c'25/8z3/8][F11/8z3/8] [^D,11/8z3/8] [G15/8z3/8] [^A,5/8z/4] [F3/2z/4][F3/2f3/8] [^D,3/2d3/8] [G15/8^d3/4z3/8] [^A,3/4z3/8] [F11/8f3/8][=F3/2f3/8] [^D,11/8d3/8] [G15/8^d3/4z3/8] [^A,5/8z3/8] [F11/8f3/8z/4][F11/8z3/8] [^D,11/8^a3/8] [G15/8c'3/8] [^A,5/8d/4] [F3/2c'3/2z3/8][F11/8z3/8] [^A7/8z3/8] [d11/8z3/8] [^A,7/8z/4] ^A3/8[F,3/2z3/8] [c'/2z3/8] [C11/8z3/8] [F11/8^a/2z3/8] [F,11/8z3/8][G7/8z/4] [C3/4z3/8] F3/8 [G,23/8z3/4] [D11/8z3/8][A9/8z3/8] F/4 D3/8 [^D,3/2z3/4] [^A,11/8z3/8]c'3/8 [C11/8z/4] [F3/2^a/2z3/8] [F,3/2z3/8] [G=a/2z3/8][c'3/8z/4] [C3/2z3/8] [F3/2^a/2z3/8] [F,11/8z3/8] [G15/8=a/2z3/8][G15/8=a/2z3/8] [C5/8z3/8] F/4 [C,23/8c'z3/4] [C11/8z3/8][G7/8=a/2z3/8] [C5/8z/4] F3/8 [G,23/8a3/4] [D11/8g3/8][G7/8=a3/8z/4] [C3/4z3/8] F3/8 [G,23/8a3/4] [D11/8g3/8]A3/8 F3/8 D/4 [^D,3/2z3/4] [^A,11/8z3/8][c'/2z3/8] [C11/8z3/8] [F11/8^a3/8z/4] [F,3/2z3/8] [G=a/2z3/8][Gz3/8] [C5/8z3/8] F/4 [=D,3/2dz3/4] [C3/2z3/8][G7/8z3/8] [C5/8z/4] F3/8 [G,23/8dz3/4] [Fz3/8][G7/8z3/8] [C5/8z/4] F3/8 [=D,3/2dz3/4] [C11/8z3/8]A3/8 F3/8 D/4 [^D,3/2z3/4] [^A,11/8z3/8]A/4 F3/8 D3/8 [^D,11/8z3/4] [^A,11/8z3/8][Gz3/8] [C5/8z3/8] F/4 [G,2z3/8] [D3/2z3/8][G7/8=a/2z3/8] [C5/8z3/8] F/4 [G,23/8a3/4] [D11/8g3/8][G15/8=a3/8] [C5/8z/4] F3/8 [C,23/8c'z3/4] [C11/8z3/8][G7/8=a3/8z/4] [C3/4z3/8] F3/8 [G,23/8a3/4] [D11/8g3/8][G7/8z3/8] [C5/8=a5/8z3/8] F/4 [C,23/8g11/2z3/8] [C15/8z3/8][C15/8z3/8] [F11/8z/4] [^A3/4z3/8] [F,3/2z3/8] [=Az3/8][F3/4z3/8] [^A7/8z3/8] [G7/8z/4] [F,3/2z3/8] [c2z3/8]G/4 [=Az3/8] [F3/2z3/8] [F,3/2z3/8] c3/8[F11/8z3/8] [^D,11/8z3/8] [G15/8z/4] [^A,3/2z3/8] [F3/2z3/8][F11/8z3/8] [^D,11/8z/4] [G2z3/8] [^A,3/4z3/8] [F3/2z3/8][F11/8f3/8] [^D,11/8d/4] [G2^d3/4z3/8] [^A,3/4z3/8] [F11/8f/2z3/8][F11/8f/4] [^D,3/2d3/8] [G15/8^d3/4z3/8] [^A,3/4z3/8] [F11/8f/2z3/8][F11/8f3/8] [^D,11/8d3/8] [G15/8^d5/8z/4] [^A,3/4z3/8] [F3/2f/2z3/8][=F11/8f3/8] [^D,11/8d3/8] [G15/8^d5/8z/4] [^A,3/4z3/8] [F3/2f/2z3/8][F11/8z3/8] [^D,11/8^a/4] [G2c'3/8] [^A,3/4d3/8] [F11/8c'11/8z3/8][=F11/8d11/8z/4] [^D,3/2z3/8] [G15/8z3/8] [^A,3/4z3/8] [F11/8z3/8][F11/8z3/8] [^D,11/8z3/8] [G15/8z/4] [^A,3/4z3/8] [F3/2z3/8][F11/8z3/8] [^D,11/8z/4] [G2z3/8] [^A,3/4z3/8] [F11/8z3/8][F11/8z/4] [^D,3/2z3/8] [G15/8z3/8] [^A,3/4z3/8] [F11/8z3/8][=F11/8d11/8z3/8] [^D,11/8z3/8] [G15/8z/4] [^A,3/4z3/8] [F3/2z3/8][F11/8d11/8z3/8] [^D,11/8z/4] [G2z3/8] [^A,3/4z3/8] [F11/8z3/8][F,11/8z3/8] [G7/8z/4] [C3/4z3/8] F3/8 [G,23/8z3/4][^D,11/8z3/8] [G15/8z3/8] [^A,5/8z/4] [F3/2z3/8] [F,3/2z3/4][F,11/8z/4] [Gz3/8] [C3/4z3/8] F3/8 [C,23/8z3/8][G5/8z/4] [c23/8z3/8] G3/8 F3/8 C3/8[^D,11/8z3/8] [G15/8z/4] [^A,3/2z3/8] [F3/2z3/8] [F,3/2z3/4][F,11/8z/4] [G3/2z3/8] [C3/2z3/8] [F/2z3/8] [G,23/8z3/4][^D,11/8z/4] [G2z3/8] [^A,3/4z3/8] [F3/2z3/8] [F,11/8z3/4][F,11/8c'5/8z3/8] [G7/8z/4] [C3/4^a3/8] [F3/8=a3] [=D,15/8z3/4][^D,11/8^a/4] [G2c'3/8] [^A,3/4d3/8] [F11/8c'11/8z3/8] [F,11/8z3/4][F,11/8c'7/8z5/8] [C3/2z3/8] [F3/2f3/2z3/8] [F,11/8z3/8] [Gz3/8][F,11/8c'7/8z5/8] [C3/2z3/8] [F11/8f11/8z3/8] [F,11/8z3/8] [G7/8z3/8][^D,11/8z3/8] [G15/8z3/8] [^A,5/8z/4] [F3/2z/4] [^a23/8z/8][F,11/8z/4] [Gz3/8] [C3/4z3/8] F3/8 [G,23/8z3/4][^D,11/8z3/8] [G15/8z/4] [^A,3/4z3/8] [F3/2z3/8] [F,11/8z3/4][^D,11/8z/4] [G2z3/8] [^A,3/4z3/8] [F11/8z3/8] [F,11/8z3/4][F,11/8z3/4] [C11/8z/4] [F3/2z3/8] [F,3/2z3/8] [Gz3/8][F,11/8z5/8] [C3/2z3/8] [F3/2z3/8] [F,11/8z3/8] [Gz3/8][^D,11/8^a3/8] [G15/8c'3/8] [^A,5/8d/4] [F3/2c'3/2z3/8] [F,3/2z3/4][F,11/8c'5/8z/4] [Gz3/8] [C3/4^a3/8] [F3/8=a3] [=D,15/8z3/8][F,11/8c'z3/4] [C11/8z/4] [F3/2f3/2z3/8] [F,3/2z3/8] [Gz3/8][F,11/8c'7/8z5/8] [C3/2z3/8] [F3/2f3/2z3/8] [F,3/2z3/8] [Gz3/8][F,11/8c'7/8z5/8] [C3/2z3/8] [F3/2^az3/8] [F,11/8z3/8] [G7/8z3/8][G3/4z3/8] [c23/8z3/8] G/4 F3/8 C3/8[^A7/8z3/8] [d11/8z3/8] [^A,7/8z/4] ^A3/8 [F3/4z3/8][^A7/8z/4] [d15/8z3/8] [^A,z3/8] ^A3/8 [F3/4z3/8][F,11/8z3/8] c3/8 A/4 F3/8 [G,3/2^a33/8f43/8z3/8][^A3/4z3/8] [F,11/8=a11/8z3/8] [=A7/8z/4] F3/8 C3/8[F,11/8c/4] c3/8 [A3/4z3/8] F3/8 [G,11/8d^a11/2=a23/8z3/8][^A5/8z/4] [F,3/2z3/8] [=Az3/8] F3/8 C3/8[F,11/8z3/8] [G7/8z/4] [C3/4z3/8] F3/8 [G,23/8z3/4][^D,11/8z3/8] [G15/8z3/8] [^A,5/8z/4] [F3/2z3/8] [F,3/2z3/4][C3/4z3/8] F3/8 [C,23/8z3/8] [C15/8z3/8] [F11/8z3/8][C3/4z3/8] F3/8 [G,11/2z5/8] [D15/8z3/8] [Gz3/4][C3/4^a3/8] [F3/8=a3] [=D,15/8z5/8] [C3/2z3/8] [G11/8z3/4][C5/8z3/8] F/4 [=D,3/2dz3/4] [C3/2z3/8] [F11/8az3/8][C3/4a3/4z3/8] F3/8 [G,11/8^a3/4z3/8] [^A7/8z3/8] [D5/8=a5/8z/4][C3/4z3/8] F3/8 [G,23/8z3/4] [D11/8z/4] [d2z3/4][C3/4z3/8] F3/8 [C,11/4z3/8] [C15/8z/4] [F3/2z3/8][C3/4z3/8] F3/8 [G,11/4z5/8] [D3/2z3/8] [d15/8z3/4][C5/8z3/8] F/4 [G,2z3/8] [D3/2z3/8] [G3/2z3/8][C3/4^a3/8] [F3/8=a3] [=D,15/8z3/8] C3/8 [C11/8G/4][C3/4z3/8] F3/8 [=D,11/8d7/8z5/8] [C3/2z3/8] [F3/2az3/8][C3/4a3/4z3/8] F3/8 [G,11/8^a5/8z/4] [^Az3/8] [D3/4=a3/4z3/8][C3/4z3/8] F3/8 [G,11/4d7/8z/4] [F3/4z3/8] [^A3/4D3/2z3/8][C3/4z3/8] F3/8 [=D,11/8d7/8z5/8] [C3/2z3/8] [F11/8az3/8][F,3/2z3/8] [c15/8z3/8] [F3/4z3/8] [^A7/8z3/8] [F,11/8=a11/4z/4][F,11/8c3/8] [c15/8z3/8] [F/4^A/4] [^AF3/8z/4] [^a23/8c'23/8f23/8z/8]




Here is the updated code:


public class MarkovMusic
{
/// <summary>
/// basic random number generator
/// </summary>
private Random mm_Rand = new Random();

/// <summary>
/// basic constructor
/// </summary>
public MarkovMusic() { }

/// <summary>
/// parses a file into a markov token dictionary
/// </summary>
/// <param name="fileName">file to parse</param>
/// <param name="n">token association size</param>
/// <param name="mm_MarkovTokens">token dictionary</param>
public void parse(String fileName, int n, Dictionary<String, List<String>> mm_MarkovTokens)
{

//get access to the input file
FileStream fs = new FileStream(fileName, FileMode.Open);

//setup the stream reader
StreamReader sr = new StreamReader(fs);

//holds tokens in work
String[] tokenArray;

//the text following the token
String followingText = "";

//create a word token placeholder for carryovers
String tokenCarry = "";

//keep reading the file until you reach the end
while (!sr.EndOfStream)
{
//check to see if a token was carried over from last loop
if (tokenCarry == "")
{
//replace token carry with the line
tokenCarry = sr.ReadLine();
}
else
{
//combine with tokenCarry
tokenCarry += " " + sr.ReadLine();
}

//split tokenCarry into tokens
tokenArray = tokenCarry.Trim().Split(' ');

//create tokens and store them into the markov Token Dictionary
for (int i = 0; i < tokenArray.Length; i++)
{
//ensure you have room to define a new token entry
//[t1][t2 t3 - tn]<[EOL]
if ((i + n) < (tokenArray.Length - 1))
{
//calculate the following text
for (int j = 1; j <= n; j++)
{
followingText += tokenArray[i + j].Trim() + " ";
}

//check to see if the token already exists
if (mm_MarkovTokens.ContainsKey(tokenArray.ToLower()))
{
//it does exist, so add the new token association
mm_MarkovTokens[tokenArray.Trim().ToLower()].Add(followingText.Trim());
}
else
{
//it doesnt exist, so create it and its token association
mm_MarkovTokens.Add(tokenArray.Trim().ToLower(), new List<String>());
mm_MarkovTokens[tokenArray.Trim().ToLower()].Add(followingText.Trim());
}
//reset following text
followingText = "";
}
else
{
//calculate the following text
for (int j = i; j < tokenArray.Length; j++)
{
followingText += tokenArray[j].Trim() + " ";
}

//carry over the training text to the next iteration
tokenCarry = followingText.Trim();
followingText = "";
break;
}
}
}

//it is likely we missed tokens near the end, do the same thing as we did above, but for them
if (tokenCarry.Length > 0)
{
tokenArray = tokenCarry.Trim().Split(' ');

//calculate the following text
for (int j = 1; j < tokenArray.Length; j++)
{
followingText += tokenArray[j].Trim() + " ";
}

if (mm_MarkovTokens.ContainsKey(tokenArray[0].ToLower()))
{
mm_MarkovTokens[tokenArray[0].Trim().ToLower()].Add(followingText.Trim());
}
else
{
mm_MarkovTokens.Add(tokenArray[0].Trim().ToLower(), new List<String>());
mm_MarkovTokens[tokenArray[0].Trim().ToLower()].Add(followingText.Trim());
}
}

//close the streams
sr.Close();
fs.Close();
}


/// <summary>
/// outputs a file with generated text based on the current token association dictionary
/// </summary>
/// <param name="fileName">file to output</param>
/// <param name="mm_MarkovTokens">token dictionary</param>
public void output(String fileName, Dictionary<String, List<String>> mm_MarkovTokens)
{
//get the first key
String token = mm_MarkovTokens.Keys.ToList<String>()[0];
List<String> tokens;
String output = token;
int index = 0;

//get 1000 token pairs
for (int i = 0; i < 100; i++)
{
if (mm_MarkovTokens.ContainsKey(token.ToLower()))
{
//find the first token
tokens = mm_MarkovTokens[token.ToLower()];
}
else
{
break;
}

//select a random index within its associated list
//(this also keeps us within distribution parameters since i allow duplicate entries)
index = mm_Rand.Next(0, tokens.Count - 1);

//get the last word from the associated list, make it the next token
token = tokens[index].Split(' ')[tokens[index].Split(' ').Length - 1];

//append the chosen token list to the output string
output += " " + tokens[index];
}

//create a file access stream
FileStream fs = new FileStream(fileName, FileMode.Create);
//create a stream writier to write the string
StreamWriter sw = new StreamWriter(fs);

//write the file
sw.Write(output);

//close the streams
sw.Close();
fs.Close();
}

/// <summary>
/// outputs a file using the given token dictionaries
/// </summary>
/// <param name="filename">file to output</param>
/// <param name="chainList">dictionaries to use</param>
public void outputMultiTier(String filename, List<Dictionary<String, List<String>>> dictionaries, int numTokens)
{
//setup temporary variables
int index = 0;
List<String> tokens = new List<string>();

//get the size of the keylist
int size = dictionaries[0].Keys.ToList<String>().Count();

//choose a key randomly
String token = dictionaries[0].Keys.ToList<String>()[mm_Rand.Next(0, size - 1)];

//variable to hold the output
String output = "";

//iterate through the desired number of tokens
for (int i = 0; i <= numTokens; i++)
{
if (dictionaries[0].ContainsKey(token.ToLower()))
{
//get the token list
tokens = dictionaries[0][token.ToLower()];
}
else
{
break;
}

//select a random index within its associated list
//(this also keeps us within distribution parameters since i allow duplicate entries)
index = mm_Rand.Next(0, tokens.Count - 1);

//get the last word from the associated list, make it the next token
token = tokens[index].Split(' ')[tokens[index].Split(' ').Length - 1];

//pull the output from the tiers
for (int j = 0; j < tokens.Count - 2; j++)
{
output += " " + outputTier(dictionaries, tokens[j], tokens[j+1], 1);
}
}

//create teh filestream and stream writer
FileStream fs = new FileStream(filename, FileMode.Create);
StreamWriter sw = new StreamWriter(fs);

//write the file
sw.Write(output);

//close the streams
sw.Close();
fs.Close();

}

/// <summary>
/// a recursive method to work its way down the tiered dictionaries and return a matching token run that begins
/// with the toke and ends with the terminator
/// </summary>
/// <param name="dictionaries">a collection of usable token dictionaries in descending order</param>
/// <param name="token">the token to match against</param>
/// <param name="tier">the current tier in the dictionaries</param>
/// <returns>the constructed token stream</returns>
private String outputTier(List<Dictionary<String, List<String>>> dictionaries, String token, String terminator, int tier)
{
List<String> tokens = new List<String>();
List<String> downSelect = new List<String>();
String output = token;
int index = 0;

//check to see if we're the lowest tier
if (tier == dictionaries.Count - 1)
{
//select the matching, add to output, return tokens
foreach (String str in dictionaries[tier][token])
{ //see if it matches the terminator
if (str.Split(' ')[str.Split(' ').Length - 1] == terminator)
{ //add to the downselect
downSelect.Add(str);
}
}

//if downselect was empty, build till you reach the terminator
if (downSelect.Count == 0)
{
//since there were no tokens downselected, generate tokens until you match
return findMatch(dictionaries[tier], token, terminator);
}
else
{
//return one of the adequate matches
return downSelect[mm_Rand.Next(0, downSelect.Count - 1)];
}
}
else //continue to break down the parse
{
//see if this dictionary has the seed token
if (dictionaries[tier].ContainsKey(token))
{
tokens = dictionaries[tier][token];
}
else
{ //nothing you can do, return the output
return output;
}

//downselect token list
foreach (String str in tokens)
{
//check for end-token matches
if (str.Split(' ')[str.Split(' ').Length - 1] == terminator)
{
downSelect.Add(str);
}
}

//find the token
if (downSelect.Count == 0)
{
//since there were no tokens downselected, generate tokens until you match
tokens = findMatch(dictionaries[tier], token, terminator).Split(' ').ToList<String>();
}
else
{
//randomly select one of the down selects
tokens = downSelect[mm_Rand.Next(0, downSelect.Count - 1)].Split(' ').ToList<String>();
}
//parse down the next tiers
for (int i = 0; i < tokens.Count - 2; i++)
{
output += " " + outputTier(dictionaries, tokens, tokens[i+1], tier + 1);
}
}

return output;
}

/// <summary>
/// builds tokens from the starting token until a matching token is found or a count limit of 100 is reached
/// </summary>
/// <param name="dictionary">the dictionary the run is built from</param>
/// <param name="startToken">the starting token</param>
/// <param name="matchToken">the ending token</param>
/// <returns></returns>
private String findMatch(Dictionary<String,List<String>> dictionary, String startToken, String matchToken)
{
String output = "";
int countLimit = 0;
List<String> tokens;
String token = startToken;
int index = 0;

//loop while you still havent found a matching end token, nor exceeded the count limit
while (!output.Contains(matchToken) || countLimit != 100 )
{
if (dictionary.ContainsKey(token.ToLower()))
{
//find the first token
tokens = dictionary[token.ToLower()];
}
else
{
countLimit++;
continue;
}

//select a random index within its associated list
//(this also keeps us within distribution parameters since i allow duplicate entries)
index = mm_Rand.Next(0, tokens.Count - 1);

//get the last word from the associated list, make it the next token
token = tokens[index].Split(' ')[tokens[index].Split(' ').Length - 1];

//append the chosen token list to the output string
output += " " + tokens[index];

countLimit++;
}

//were you able to find a completing string?
if (!output.Contains(matchToken))
{
//couldnt not find string a string in time
return "";
}
else
{
//return the completed string
return output.Substring(0, output.IndexOf(matchToken));
}
}
}


[update: removed some things that would cause tokens to merge and a potential infinite loop edge case]

the code that i used to call MarkovMusic:



static void Main(string[] args)
{
MarkovMusic parser = new MarkovMusic();
Dictionary<String, List<String>> markov5 = new Dictionary<String,List<String>>();
Dictionary<String, List<String>> markov4 = new Dictionary<String, List<String>>();
Dictionary<String, List<String>> markov3 = new Dictionary<String, List<String>>();
Dictionary<String, List<String>> markov2 = new Dictionary<String, List<String>>();


parser.parse("input.txt",5, markov5);
parser.parse("input2.txt",5, markov5);
parser.parse("input3.txt",5, markov5);
parser.parse("input4.txt",5, markov5);
parser.parse("input5.txt",5, markov5);

parser.parse("input.txt", 4, markov4);
parser.parse("input2.txt", 4, markov4);
parser.parse("input3.txt", 4, markov4);
parser.parse("input4.txt", 4, markov4);
parser.parse("input5.txt", 4, markov4);

parser.parse("input.txt", 3, markov3);
parser.parse("input2.txt", 3, markov3);
parser.parse("input3.txt", 3, markov3);
parser.parse("input4.txt", 3, markov3);
parser.parse("input5.txt", 3, markov3);

parser.parse("input.txt", 2, markov2);
parser.parse("input2.txt", 2, markov2);
parser.parse("input3.txt", 2, markov2);
parser.parse("input4.txt", 2, markov2);
parser.parse("input5.txt", 2, markov2);



List<Dictionary<String, List<String>>> chainList = new List<Dictionary<string, List<string>>>();

chainList.Add(markov5);
chainList.Add(markov4);
chainList.Add(markov3);
chainList.Add(markov2);

parser.outputMultiTier("output.txt", chainList,10);

}







[quote name='Net Gnome' timestamp='1313171642' post='4848339']
What if a run association isnt found? Then you build on your current token tier until you encounter a matching end token that completes the theme selection and then move on from there.

So that is the basis I'm thinking of, and it should produce a better result than my last video, but how much better remains to be seen. Hopefully I can get this coded this weekend, but we'll see.


I think you're at the point where the markov-chain isn't going to be enough on its own. I'm not a musician, but I would think you'll need to check the 'sanity' of the following:

1. Does the addition of sequence [t2 t3 t4 ... tN] stay in key?
2. Does the addition cause the sequence to lose tempo?
3. Does the sequence belong to the apporpriate section of the song? (most songs have middles, endings, etc..)

So, rather than just randomly pick from the chain, you need to pick, check, and if failed, pick a new one.

Something that would be really nice would be multi-voice compositions. So a melody voice and a bass voice for example. Basically each value in the markov chain would contain 2 values-- one for each voice. Could be tricky due to tempo/timing though...
[/quote]


By itself, you're right on those counts about the markov generator. I've taken it about as far as you can so that the markov 'intelligently' selects tokens based on desired token patterns.

Issue 1) it can be resolved in that as long as the samples are in the same key, the output will remain in the same key. However, accidentals (notes that are purposefully off-key) can still cause off-key notes and would need to be looked-out for. (in my latest ones i actually replaced an input sample because it had waaay too many accidentals in it such that the key should have never been defined as C (probably C minor).

Issue 2) if you allow the the generator to vary its tempo mid-song, yes this is an issue. you can also check tokens for notes that are of too-different timing (i.e., your song is based of of quarter notes, you dont want too many 16th notes showing up, but 8th notes may be ok). It would require adding more ABC related note information, but shouldn't be too hard.

Issue 3) thats could be done with some sort of weighting value as the parse moves forward through the song it gets interpolated between its distance from start and end between two threshold values. That would allow you to weight tokens for a section of the song and would be easy to add.
found a bug in the above code that actually changes the way it builds the music... I'm working on the correction. I'll post it here when complete.

[update] turns out that the bug actually produced a better music flow, but didn't necessarily build the music as per the concept. Here is the revised code, however i still recommend the "buggy" previous one as it created a more improvised / alternate feel of the source files

public class MarkovMusic
{
/// <summary>
/// basic random number generator
/// </summary>
private Random mm_Rand = new Random();

/// <summary>
/// basic constructor
/// </summary>
public MarkovMusic() { }

/// <summary>
/// parses a file into a markov token dictionary
/// </summary>
/// <param name="fileName">file to parse</param>
/// <param name="n">token association size</param>
/// <param name="mm_MarkovTokens">token dictionary</param>
public void parse(String fileName, int n, Dictionary<String, List<String>> mm_MarkovTokens)
{

//get access to the input file
FileStream fs = new FileStream(fileName, FileMode.Open);

//setup the stream reader
StreamReader sr = new StreamReader(fs);

//holds tokens in work
String[] tokenArray;

//the text following the token
String followingText = "";

//create a word token placeholder for carryovers
String tokenCarry = "";

//keep reading the file until you reach the end
while (!sr.EndOfStream)
{
//check to see if a token was carried over from last loop
if (tokenCarry == "")
{
//replace token carry with the line
tokenCarry = sr.ReadLine();
}
else
{
//combine with tokenCarry
tokenCarry += " " + sr.ReadLine();
}

//split tokenCarry into tokens
tokenArray = tokenCarry.Trim().Split(' ');

//create tokens and store them into the markov Token Dictionary
for (int i = 0; i < tokenArray.Length; i++)
{
//ensure you have room to define a new token entry
//[t1][t2 t3 - tn]<[EOL]
if ((i + n) < (tokenArray.Length - 1))
{
//calculate the following text
for (int j = 1; j <= n; j++)
{
followingText += tokenArray[i + j].Trim() + " ";
}

//check to see if the token already exists
if (mm_MarkovTokens.ContainsKey(tokenArray.ToLower()))
{
//it does exist, so add the new token association
mm_MarkovTokens[tokenArray.Trim().ToLower()].Add(followingText.Trim());
}
else
{
//it doesnt exist, so create it and its token association
mm_MarkovTokens.Add(tokenArray.Trim().ToLower(), new List<String>());
mm_MarkovTokens[tokenArray.Trim().ToLower()].Add(followingText.Trim());
}
//reset following text
followingText = "";
}
else
{
//calculate the following text
for (int j = i; j < tokenArray.Length; j++)
{
followingText += tokenArray[j].Trim() + " ";
}

//carry over the training text to the next iteration
tokenCarry = followingText.Trim();
followingText = "";
break;
}
}
}

//it is likely we missed tokens near the end, do the same thing as we did above, but for them
if (tokenCarry.Length > 0)
{
tokenArray = tokenCarry.Trim().Split(' ');

//calculate the following text
for (int j = 1; j < tokenArray.Length; j++)
{
followingText += tokenArray[j].Trim() + " ";
}

if (mm_MarkovTokens.ContainsKey(tokenArray[0].ToLower()))
{
mm_MarkovTokens[tokenArray[0].Trim().ToLower()].Add(followingText.Trim());
}
else
{
mm_MarkovTokens.Add(tokenArray[0].Trim().ToLower(), new List<String>());
mm_MarkovTokens[tokenArray[0].Trim().ToLower()].Add(followingText.Trim());
}
}

//close the streams
sr.Close();
fs.Close();
}


/// <summary>
/// outputs a file with generated text based on the current token association dictionary
/// </summary>
/// <param name="fileName">file to output</param>
/// <param name="mm_MarkovTokens">token dictionary</param>
public void output(String fileName, Dictionary<String, List<String>> mm_MarkovTokens)
{
//get the first key
String token = mm_MarkovTokens.Keys.ToList<String>()[0];
List<String> tokens;
String output = token;
int index = 0;

//get 1000 token pairs
for (int i = 0; i < 100; i++)
{
if (mm_MarkovTokens.ContainsKey(token.ToLower()))
{
//find the first token
tokens = mm_MarkovTokens[token.ToLower()];
}
else
{
break;
}

//select a random index within its associated list
//(this also keeps us within distribution parameters since i allow duplicate entries)
index = mm_Rand.Next(0, tokens.Count - 1);

//get the last word from the associated list, make it the next token
token = tokens[index].Split(' ')[tokens[index].Split(' ').Length - 1];

//append the chosen token list to the output string
output += " " + tokens[index];
}

//create a file access stream
FileStream fs = new FileStream(fileName, FileMode.Create);
//create a stream writier to write the string
StreamWriter sw = new StreamWriter(fs);

//write the file
sw.Write(output);

//close the streams
sw.Close();
fs.Close();
}

/// <summary>
/// outputs a file using the given token dictionaries
/// </summary>
/// <param name="filename">file to output</param>
/// <param name="chainList">dictionaries to use</param>
public void outputMultiTier(String filename, List<Dictionary<String, List<String>>> dictionaries, int numTokens)
{
//setup temporary variables
int index = 0;
List<String> tokens = new List<string>();

//get the size of the keylist
int size = dictionaries[0].Keys.ToList<String>().Count();

//choose a key randomly
String token = dictionaries[0].Keys.ToList<String>()[mm_Rand.Next(0, size - 1)];

//variable to hold the output
String output = "";

//iterate through the desired number of tokens
for (int i = 0; i <= numTokens; i++)
{
if (dictionaries[0].ContainsKey(token.Trim()))
{
//get the token list
tokens = dictionaries[0][token.Trim()];
}
else
{
break;
}

//select a random index within its associated list
//(this also keeps us within distribution parameters since i allow duplicate entries)
index = mm_Rand.Next(0, tokens.Count - 1);

//pull the output from the tiers
for (int j = 0; j < tokens[index].Split(' ').Length-2; j++)
{
int l = tokens[index].Split(' ').Length;
output += " " + outputTier(dictionaries, tokens[index].Split(' ')[j], tokens[index].Split(' ')[j + 1], 1);
}

//get the last word from the associated list, make it the next token
token = tokens[index].Split(' ')[tokens[index].Split(' ').Length - 1];
}

//create teh filestream and stream writer
FileStream fs = new FileStream(filename, FileMode.Create);
StreamWriter sw = new StreamWriter(fs);

//write the file
sw.WriteLine(output);

//close the streams
sw.Close();
fs.Close();

}

/// <summary>
/// a recursive method to work its way down the tiered dictionaries and return a matching token run that begins
/// with the toke and ends with the terminator
/// </summary>
/// <param name="dictionaries">a collection of usable token dictionaries in descending order</param>
/// <param name="token">the token to match against</param>
/// <param name="tier">the current tier in the dictionaries</param>
/// <returns>the constructed token stream</returns>
private String outputTier(List<Dictionary<String, List<String>>> dictionaries, String token, String terminator, int tier)
{
List<String> tokens = new List<String>();
List<String> downSelect = new List<String>();
String output = token;
int index = 0;

//check to see if we're the lowest tier
if (tier == dictionaries.Count - 1)
{
if (dictionaries[tier].ContainsKey(token.Trim()))
{
//select the matching, add to output, return tokens
foreach (String str in dictionaries[tier][token.Trim()])
{ //see if it matches the terminator
if (str.Split(' ')[str.Split(' ').Length - 1] == terminator)
{ //add to the downselect
downSelect.Add(str);
}
}
}
else
{
return output;
}

//if downselect was empty, build till you reach the terminator
if (downSelect.Count == 0)
{
//since there were no tokens downselected, generate tokens until you match
return findMatch(dictionaries[tier], token, terminator);
}
else
{
//return one of the adequate matches
return downSelect[mm_Rand.Next(0, downSelect.Count - 1)];
}
}
else //continue to break down the parse
{
//see if this dictionary has the seed token
if (dictionaries[tier].ContainsKey(token.ToLower().Trim()))
{
tokens = dictionaries[tier][token.ToLower().Trim()];
}
else
{ //nothing you can do, return the output
return output;
}

//downselect token list
foreach (String str in tokens)
{
//check for end-token matches
if (str.Split(' ')[str.Split(' ').Length - 1] == terminator)
{
downSelect.Add(str);
}
}

//find the token
if (downSelect.Count == 0)
{
//since there were no tokens downselected, generate tokens until you match
tokens = findMatch(dictionaries[tier], token, terminator).Split(' ').ToList<String>();//(token + " " + terminator).Split(' ').ToList<String>();
}
else
{
//randomly select one of the down selects
tokens = downSelect[mm_Rand.Next(0, downSelect.Count - 1)].Split(' ').ToList<String>();
}
//parse down the next tiers
for (int i = 0; i < tokens.Count - 2; i++)
{
//if (tokens == "")
//{
//i++;
//}

output += " " +outputTier(dictionaries, tokens, tokens[i+1], tier + 1);
}
}

return output;
}

/// <summary>
/// builds tokens from the starting token until a matching token is found or a count limit of 100 is reached
/// </summary>
/// <param name="dictionary">the dictionary the run is built from</param>
/// <param name="startToken">the starting token</param>
/// <param name="matchToken">the ending token</param>
/// <returns></returns>
private String findMatch(Dictionary<String,List<String>> dictionary, String startToken, String matchToken)
{
String output = "";
int countLimit = 0;
List<String> tokens;
String token = startToken;
int index = 0;

//loop while you still havent found a matching end token, nor exceeded the count limit
while (!output.Contains(matchToken.Trim()) && countLimit < 100 )
{
if (dictionary.ContainsKey(token.ToLower().Trim()))
{
//find the first token
tokens = dictionary[token.ToLower().Trim()];
}
else
{
countLimit++;
continue;
}

//select a random index within its associated list
//(this also keeps us within distribution parameters since i allow duplicate entries)
index = mm_Rand.Next(0, tokens.Count - 1);

//get the last word from the associated list, make it the next token
token = tokens[index].Split(' ')[tokens[index].Split(' ').Length - 1];

//append the chosen token list to the output string
output += " " + tokens[index];

countLimit++;
}

//were you able to find a completing string?
if (!output.Contains(matchToken))
{
//couldnt not find string a string in time
return "";
}
else
{
//return the completed string
int bracketOpen = 0;
int bracketClosed = 0;
for (int i = 0; i < output.Length; i++)
{
if (output == '[')
bracketOpen++;
if (output == ']')
bracketClosed++;
}

//return output.Substring(0, output.IndexOf(matchToken.Trim())+1);
if (bracketOpen == bracketClosed)
return output;
else
return output + "]";
}
}
}

This topic is closed to new replies.

Advertisement