Skip to content

Commit

Permalink
Refactor and more tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Josh Symes committed Jul 23, 2023
1 parent 383914b commit 323979b
Show file tree
Hide file tree
Showing 8 changed files with 368 additions and 241 deletions.
25 changes: 21 additions & 4 deletions henon2midi/base.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,34 @@
from mido import MetaMessage, MidiFile, MidiTrack, bpm2tempo

from henon2midi.henon_midi_generator import HenonMidiGenerator
from henon2midi.data_point_to_midi_conversion import (
create_midi_messages_from_data_point,
)
from henon2midi.henon_equations import RadiallyExpandingHenonMappingsGenerator


def create_midi_file_from_midi_generator(
henon_midi_generator: HenonMidiGenerator,
def create_midi_file_from_data_generator(
henon_midi_generator: RadiallyExpandingHenonMappingsGenerator,
ticks_per_beat: int = 960,
bpm: int = 120,
notes_per_beat: int = 4,
sustain: bool = False,
clip: bool = False,
x_midi_parameter_mappings_set: set[str] = {"note"},
y_midi_parameter_mappings_set: set[str] = {"velocity", "pan"},
) -> MidiFile:
mid = MidiFile(ticks_per_beat=ticks_per_beat)
track = MidiTrack()
mid.tracks.append(track)
tempo = bpm2tempo(bpm)
track.append(MetaMessage("set_tempo", tempo=tempo))
track.extend(henon_midi_generator.generate_all_midi_messages())
for datapoint in henon_midi_generator.radially_expending_henon_mappings_generator():
midi_messages = create_midi_messages_from_data_point(
datapoint,
duration_ticks=int(ticks_per_beat / notes_per_beat),
sustain=sustain,
clip=clip,
x_midi_parameter_mappings=x_midi_parameter_mappings_set,
y_midi_parameter_mappings=y_midi_parameter_mappings_set,
)
track.extend(midi_messages)
return mid
71 changes: 45 additions & 26 deletions henon2midi/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
from mido import Message

from henon2midi.ascii_art import AsciiArtCanvas
from henon2midi.base import create_midi_file_from_midi_generator
from henon2midi.henon_midi_generator import HenonMidiGenerator
from henon2midi.base import create_midi_file_from_data_generator
from henon2midi.data_point_to_midi_conversion import (
create_midi_messages_from_data_point,
)
from henon2midi.henon_equations import RadiallyExpandingHenonMappingsGenerator
from henon2midi.math import rescale_number_to_range
from henon2midi.midi import (
MidiMessagePlayer,
Expand Down Expand Up @@ -173,21 +176,21 @@ def cli(
)
click.echo(options_string)

henon_midi_generator = HenonMidiGenerator(
a_parameter=a_parameter,
iterations_per_orbit=iterations_per_orbit,
starting_radius=starting_radius,
radial_step=radial_step,
note_length_ticks=ticks_per_beat // notes_per_beat,
sustain=sustain,
clip=clip,
x_midi_parameter_mappings=x_midi_parameter_mappings_set,
y_midi_parameter_mappings=y_midi_parameter_mappings_set,
)

if midi_output_file_name:
mid = create_midi_file_from_midi_generator(
henon_midi_generator, ticks_per_beat=ticks_per_beat, bpm=bpm
mid = create_midi_file_from_data_generator(
RadiallyExpandingHenonMappingsGenerator(
a_parameter=a_parameter,
iterations_per_orbit=iterations_per_orbit,
starting_radius=starting_radius,
radial_step=radial_step,
),
ticks_per_beat=ticks_per_beat,
bpm=bpm,
notes_per_beat=notes_per_beat,
sustain=sustain,
clip=clip,
x_midi_parameter_mappings_set=x_midi_parameter_mappings_set,
y_midi_parameter_mappings_set=y_midi_parameter_mappings_set,
)
mid.save(midi_output_file_name)

Expand All @@ -200,15 +203,31 @@ def cli(
art_string = ""

if midi_output_name:
hennon_mappings_generator = RadiallyExpandingHenonMappingsGenerator(
a_parameter=a_parameter,
iterations_per_orbit=iterations_per_orbit,
starting_radius=starting_radius,
radial_step=radial_step,
)
midi_message_player = MidiMessagePlayer(
midi_output_name=midi_output_name, ticks_per_beat=ticks_per_beat, bpm=bpm
)

while True:
messages = henon_midi_generator.next_midi_messages()
current_iteration = henon_midi_generator.current_iteration
current_orbit = (
current_iteration // henon_midi_generator.iterations_per_orbit
messages = create_midi_messages_from_data_point(
hennon_mappings_generator.generate_next_data_point(),
duration_ticks=int(ticks_per_beat / notes_per_beat),
sustain=sustain,
clip=clip,
x_midi_parameter_mappings=x_midi_parameter_mappings_set,
y_midi_parameter_mappings=y_midi_parameter_mappings_set,
)

current_iteration = hennon_mappings_generator.current_iteration
current_orbit = hennon_mappings_generator.current_orbital_iteration
current_data_point = hennon_mappings_generator.current_data_point
is_new_orbit = hennon_mappings_generator.is_new_orbit()

try:
midi_message_player.send(messages)
except KeyboardInterrupt:
Expand All @@ -223,15 +242,15 @@ def cli(
exit()
current_state_string = (
f"Current iteration: {current_iteration}\n"
f"Current orbit: {current_orbit + 1}\n"
f"Current orbit: {current_orbit}\n"
"\n"
)

if draw_ascii_art:
art_string = get_ascii_art(
henon_midi_generator.current_data_point,
current_data_point,
current_iteration,
henon_midi_generator.iterations_per_orbit,
is_new_orbit,
ascii_art_canvas,
)

Expand All @@ -244,8 +263,8 @@ def cli(

def get_ascii_art(
data_point: tuple[float, float],
current_iteration,
iterations_per_orbit,
current_iteration: int,
is_new_orbit: bool,
ascii_art_canvas: AsciiArtCanvas,
) -> str:
if current_iteration == 0:
Expand All @@ -260,6 +279,6 @@ def get_ascii_art(
)
ascii_art_canvas.draw_point(draw_point_coord[0], draw_point_coord[1], ".")

if current_iteration % iterations_per_orbit == 0:
if is_new_orbit:
ascii_art_canvas.set_color("random")
return ascii_art_canvas.generate_string()
104 changes: 104 additions & 0 deletions henon2midi/data_point_to_midi_conversion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
from mido import Message

from henon2midi.math import rescale_number_to_range


def create_midi_messages_from_data_point(
datapoint: tuple[float, float],
duration_ticks: float = 960,
sustain: bool = False,
clip: bool = False,
x_midi_parameter_mappings: set[str] = {"note"},
y_midi_parameter_mappings: set[str] = {"velocity", "pan"},
) -> list[Message]:
x = datapoint[0]
y = datapoint[1]

midi_values = {
"note": 64,
"velocity": 64,
"pan": 64,
}

for x_midi_parameter_mapping in x_midi_parameter_mappings:
if (x > 1.0 or x < -1.0) and not clip:
midi_values[x_midi_parameter_mapping] = 0
else:
midi_values[x_midi_parameter_mapping] = midi_value_from_data_point_value(x)

for y_midi_parameter_mapping in y_midi_parameter_mappings:
if (y > 1.0 or y < -1.0) and not clip:
midi_values[y_midi_parameter_mapping] = 0
else:
midi_values[y_midi_parameter_mapping] = midi_value_from_data_point_value(y)

note_on = Message(
"note_on",
note=midi_values["note"],
velocity=midi_values["velocity"],
)
note_off = Message(
"note_off",
note=midi_values["note"],
velocity=midi_values["velocity"],
time=duration_ticks,
)

note_messages = [note_on, note_off]
pre_note_messages = []
post_note_messages = []

if "pan" in x_midi_parameter_mappings or "pan" in y_midi_parameter_mappings:
pan = Message(
"control_change",
control=10,
value=midi_values["pan"],
)
pre_note_messages.append(pan)

reset_pan = Message(
"control_change",
control=10,
value=64,
)
post_note_messages.append(reset_pan)

if sustain:
sustain_on_msg = Message(
"control_change",
control=64,
value=127,
)
pre_note_messages.append(sustain_on_msg)
else:
sustain_off_msg = Message(
"control_change",
control=64,
value=0,
)
post_note_messages.append(sustain_off_msg)

messages = pre_note_messages + note_messages + post_note_messages

return messages


def midi_value_from_data_point_value(
x: float,
data_point_range: tuple[float, float] = (-1.0, 1.0),
midi_range: tuple[int, int] = (0, 127),
) -> int:
min_midi_value = midi_range[0]
max_midi_value = midi_range[1]

min_data_point_value = data_point_range[0]
max_data_point_value = data_point_range[1]

return round(
rescale_number_to_range(
x,
(min_data_point_value, max_data_point_value),
(min_midi_value, max_midi_value),
clip_value=True,
)
)
89 changes: 78 additions & 11 deletions henon2midi/henon_equations.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,81 @@ def henon_mapping_generator(
yield x, y


def radially_expanding_henon_mappings_generator(
a_parameter: float,
iterations_per_orbit: int = 32,
starting_radius: float = 0.0,
radial_step: float = 0.1,
) -> Generator[tuple[float, float], None, None]:
radius = starting_radius
while radius <= 1:
radius += radial_step
for _ in range(iterations_per_orbit):
yield from henon_mapping_generator(a_parameter, radius, radius)
class RadiallyExpandingHenonMappingsGenerator:
def __init__(
self,
a_parameter: float,
iterations_per_orbit: int = 100,
starting_radius: float = 0.1,
radial_step: float = 0.05,
):
self.a_parameter = a_parameter
self.iterations_per_orbit = iterations_per_orbit
self.starting_radius = starting_radius
self.radial_step = radial_step
self.henon_mapping_generator = henon_mapping_generator(
a_parameter, starting_radius, starting_radius
)
self.data_point_generator = self.radially_expending_henon_mappings_generator()
self.current_orbital_iteration = 0
self.current_iteration = 0
self.iteration_of_current_orbit = 0
self.current_radius = starting_radius
self.current_data_point = (starting_radius, starting_radius)

def generate_next_data_point(self) -> tuple[float, float]:
try:
data_point = next(self.data_point_generator)
except StopIteration:
self.restart_data_point_generator()
data_point = next(self.data_point_generator)
return data_point

def restart_data_point_generator(self):
self.data_point_generator = self.radially_expending_henon_mappings_generator()

def radially_expending_henon_mappings_generator(
self,
) -> Generator[tuple[float, float], None, None]:
self._reset_to_starting_radius()
while self.current_radius <= 1:
self.iteration_of_current_orbit = 0
self.current_orbital_iteration += 1
self.henon_mapping_generator = henon_mapping_generator(
self.a_parameter, self.current_radius, self.current_radius
)

while self.iteration_of_current_orbit < self.iterations_per_orbit:
try:
data_point = next(self.henon_mapping_generator)
except StopIteration:
break
self.current_data_point = data_point
self.current_iteration += 1
self.current_data_point = data_point
self.iteration_of_current_orbit += 1
yield data_point

self.current_radius += self.radial_step

def _reset_to_starting_radius(self):
self.current_radius = self.starting_radius
self.current_data_point = (self.starting_radius, self.starting_radius)

def get_current_iteration(self) -> int:
return self.current_iteration

def get_current_data_point(self) -> tuple[float, float]:
return self.current_data_point

def get_current_radius(self) -> float:
return self.current_radius

def get_current_orbital_iteration(self) -> int:
return self.current_orbital_iteration

def get_iteration_of_current_orbit(self) -> int:
return self.iteration_of_current_orbit

def is_new_orbit(self) -> bool:
return self.iteration_of_current_orbit == 1
Loading

0 comments on commit 323979b

Please sign in to comment.