Created
January 31, 2022 23:34
-
-
Save somacdivad/fdcb59f52037269472804d1b5a08cab7 to your computer and use it in GitHub Desktop.
Create music from graphs by converting the eigenvalues of the normalized Laplacian matrix to frequencies
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
import LinearAlgebra as LinAlg | |
import SparseArrays | |
import Graphs | |
import WAV | |
function normalized_laplacian_matrix(g::Graphs.SimpleGraph) | |
A = Graphs.CombinatorialAdjacency(Graphs.adjacency_matrix(g).+ 0.0) | |
 = Graphs.NormalizedAdjacency(A) | |
L̂ = Graphs.NormalizedLaplacian(Â) | |
return SparseArrays.sparse(L̂) | |
end | |
function normalized_laplacian_spectrum(g::Graphs.SimpleGraph) | |
return LinAlg.eigvals(LinAlg.Matrix(normalized_laplacian_matrix(g))) | |
end | |
function spectrum_to_frequencies(eigenvalues; zero_freq=440) | |
# Compute the frequency for each eigenvalue | |
# 0 gets mapped to `zero_freq`, | |
# 1 gets mapped to zero_freq + 1 octave (2 * zero_freq), | |
# 2 gets mapped to zero_freq + 2 otaves (4 * zero_freq), | |
# etc. | |
frequency(x) = 2^x * zero_freq | |
return frequency.(eigenvalues) | |
end | |
function sine_wave(frequency::Float64; duration::Float64=1.0, Fs::Float64=8e3) | |
# Create a range with length `duration` divided into `Fs` samples | |
t = 0.0:1/Fs:prevfloat(duration) | |
# Calculate the values of a sine wave with frequency `frequency` | |
# over each sample in `t` | |
return sin.(2π * frequency * t) * 0.1 | |
end | |
function frequencies_to_tones(frequencies::Vector{Float64}; duration::Float64=1.0, Fs::Float64=8e3) | |
return sine_wave.(frequencies, duration=duration, Fs=Fs) | |
end | |
function make_tones(g::Graphs.SimpleGraph; duration::Float64=1.0, Fs::Float64=8e3) | |
# NOTE: Repeated eigenvalues in the spectrum result in repeated tones | |
spectrum = normalized_laplacian_spectrum(g) | |
frequencies = spectrum_to_frequencies(spectrum) | |
return frequencies_to_tones(frequencies, duration=duration, Fs=Fs) | |
end | |
function write_chord(tones::Vector{Vector{Float64}}, filename::String) | |
chord = sum.(zip(tones...)) | |
WAV.wavwrite(chord, filename) | |
end | |
function write_scale(tones::Vector{Vector{Float64}}, filename::String) | |
unique!(tones) | |
WAV.wavwrite(tones[1], filename) | |
for i in 2:length(tones) | |
WAV.wavappend(tones[i], filename) | |
end | |
end | |
# Change the graph below to listen to a different graph (original: Graphs.SimpleGraph(5, 9)) | |
# NOTE: You can find all the graph generators in Graphs.jl at https://juliagraphs.org/Graphs.jl/dev/generators/#Graph-Generators | |
G = Graphs.SimpleGraph(5, 9) | |
# Some interesting graphs to try: | |
# G = Graphs.cycle_graph(8) | |
# G = Graphs.path_graph(8) | |
# G = Graphs.barabasi_albert(6, 3) | |
# G = Graphs.dorogovtsev_mendes(6) | |
# Write the graph's "scale" to a WAV file and then play it | |
short_tones = make_tones(G, duration=0.25) | |
write_scale(short_tones, "scale.wav") | |
WAV.wavplay("scale.wav") | |
# Write the graph's "chord" to a WAV file and then play it | |
long_tones = make_tones(G) | |
write_chord(long_tones, "chord.wav") | |
WAV.wavplay("chord.wav") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment