Skip to content

Commit

Permalink
Add ability to control range of midi, values, more refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
Josh Symes committed Jul 23, 2023
1 parent 323979b commit c5ddff5
Show file tree
Hide file tree
Showing 6 changed files with 232 additions and 73 deletions.
38 changes: 22 additions & 16 deletions henon2midi/base.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from mido import MetaMessage, MidiFile, MidiTrack, bpm2tempo
from mido import MidiFile

from henon2midi.data_point_to_midi_conversion import (
create_midi_messages_from_data_point,
)
from henon2midi.henon_equations import RadiallyExpandingHenonMappingsGenerator
from henon2midi.midi import create_midi_file_from_messages


def create_midi_file_from_data_generator(
Expand All @@ -15,20 +16,25 @@ def create_midi_file_from_data_generator(
clip: bool = False,
x_midi_parameter_mappings_set: set[str] = {"note"},
y_midi_parameter_mappings_set: set[str] = {"velocity", "pan"},
source_range_x: tuple[float, float] = (-1.0, 1.0),
source_range_y: tuple[float, float] = (-1.0, 1.0),
midi_range_x: tuple[int, int] = (0, 127),
midi_range_y: tuple[int, int] = (0, 127),
) -> 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))
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,
messages = []
for datapoint in henon_midi_generator:
messages.extend(
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,
source_range_x=source_range_x,
source_range_y=source_range_y,
midi_range_x=midi_range_x,
midi_range_y=midi_range_y,
)
)
track.extend(midi_messages)
return mid
return create_midi_file_from_messages(messages, ticks_per_beat, bpm)
119 changes: 103 additions & 16 deletions henon2midi/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,20 @@
show_default=True,
type=str,
)
@click.option(
"--x-midi-value-range",
default="0,127",
help="The MIDI value range for the x data point.",
show_default=True,
type=str,
)
@click.option(
"--y-midi-value-range",
default="0,127",
help="The MIDI value range for the y data point.",
show_default=True,
type=str,
)
@click.option(
"-r",
"--starting-radius",
Expand Down Expand Up @@ -117,6 +131,12 @@
help="Clip the MIDI messages to the range of the MIDI parameter.",
type=bool,
)
@click.option(
"--continual-loop",
is_flag=True,
help="Loop back to start when Henon data is exhausted.",
type=bool,
)
def cli(
a_parameter: float,
iterations_per_orbit: int,
Expand All @@ -126,12 +146,15 @@ def cli(
notes_per_beat: int,
x_midi_parameter_mappings: str,
y_midi_parameter_mappings: str,
x_midi_value_range: str,
y_midi_value_range: str,
starting_radius: float,
radial_step: float,
out: str,
draw_ascii_art: bool,
sustain: bool,
clip: bool,
continual_loop: bool,
):
"""An application that generates midi from procedurally generated Henon mappings."""

Expand All @@ -150,6 +173,26 @@ def cli(
notes_per_beat = notes_per_beat
x_midi_parameter_mappings_set = set(x_midi_parameter_mappings.split(","))
y_midi_parameter_mappings_set = set(y_midi_parameter_mappings.split(","))
x_midi_value_range_split = x_midi_value_range.split(",")
if len(x_midi_value_range_split) != 2:
raise ValueError(
"x_midi_value_range must be a comma separated list of 2 values"
)
else:
midi_range_x = (
int(x_midi_value_range_split[0]),
int(x_midi_value_range_split[1]),
)
y_midi_value_range_split = y_midi_value_range.split(",")
if len(y_midi_value_range_split) != 2:
raise ValueError(
"y_midi_value_range must be a comma separated list of 2 values"
)
else:
midi_range_y = (
int(y_midi_value_range_split[0]),
int(y_midi_value_range_split[1]),
)
starting_radius = starting_radius
radial_step = radial_step
draw_ascii_art = draw_ascii_art
Expand All @@ -166,6 +209,8 @@ def cli(
f"\tnotes per beat: {notes_per_beat}\n"
f"\tx midi parameter mappings: {x_midi_parameter_mappings_set}\n"
f"\ty midi parameter mappings: {y_midi_parameter_mappings_set}\n"
f"\tx midi value range: {midi_range_x}\n"
f"\ty midi value range: {midi_range_y}\n"
f"\tstarting radius: {starting_radius}\n"
f"\tradial step: {radial_step}\n"
f"\tout: {midi_output_file_name}\n"
Expand All @@ -191,6 +236,10 @@ def cli(
clip=clip,
x_midi_parameter_mappings_set=x_midi_parameter_mappings_set,
y_midi_parameter_mappings_set=y_midi_parameter_mappings_set,
source_range_x=(-1.0, 1.0),
source_range_y=(-1.0, 1.0),
midi_range_x=midi_range_x,
midi_range_y=midi_range_y,
)
mid.save(midi_output_file_name)

Expand All @@ -200,7 +249,6 @@ def cli(
ascii_art_canvas = AsciiArtCanvas(
ascii_art_canvas_width, ascii_art_canvas_height
)
art_string = ""

if midi_output_name:
hennon_mappings_generator = RadiallyExpandingHenonMappingsGenerator(
Expand All @@ -221,13 +269,21 @@ def cli(
clip=clip,
x_midi_parameter_mappings=x_midi_parameter_mappings_set,
y_midi_parameter_mappings=y_midi_parameter_mappings_set,
source_range_x=(-1.0, 1.0),
source_range_y=(-1.0, 1.0),
midi_range_x=midi_range_x,
midi_range_y=midi_range_y,
)

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()

if not continual_loop:
if hennon_mappings_generator.get_times_reset() > 0:
break

try:
midi_message_player.send(messages)
except KeyboardInterrupt:
Expand All @@ -247,12 +303,15 @@ def cli(
)

if draw_ascii_art:
art_string = get_ascii_art(
art_string = build_art_string(
current_data_point,
ascii_art_canvas,
current_iteration,
is_new_orbit,
ascii_art_canvas,
clip=clip,
)
else:
art_string = ""

click.clear()
screen_render = (
Expand All @@ -261,24 +320,52 @@ def cli(
click.echo(screen_render)


def get_ascii_art(
data_point: tuple[float, float],
def build_art_string(
new_data_point: tuple[float, float],
ascii_art_canvas: AsciiArtCanvas,
current_iteration: int,
is_new_orbit: bool,
ascii_art_canvas: AsciiArtCanvas,
clip: bool = False,
) -> str:
if current_iteration == 0:
if current_iteration == 1:
ascii_art_canvas.clear()
x = data_point[0]
y = data_point[1]
draw_point_coord = (
round(rescale_number_to_range(x, (-1.0, 1.0), (0, ascii_art_canvas.width - 1))),
round(
rescale_number_to_range(y, (-1.0, 1.0), (0, ascii_art_canvas.height - 1))
),
draw_data_point_on_canvas(
new_data_point,
ascii_art_canvas,
is_new_orbit,
clip=clip,
)
ascii_art_canvas.draw_point(draw_point_coord[0], draw_point_coord[1], ".")
return ascii_art_canvas.generate_string()


def draw_data_point_on_canvas(
data_point: tuple[float, float],
ascii_art_canvas: AsciiArtCanvas,
is_new_orbit: bool,
clip: bool = False,
):
x = data_point[0]
y = data_point[1]
if is_new_orbit:
ascii_art_canvas.set_color("random")
return ascii_art_canvas.generate_string()
try:
x_canvas_coord = round(
rescale_number_to_range(
x,
(-1.0, 1.0),
(0, ascii_art_canvas.width - 1),
clip_value=clip,
)
)
y_canvas_coord = round(
rescale_number_to_range(
y,
(-1.0, 1.0),
(0, ascii_art_canvas.height - 1),
clip_value=clip,
)
)
except ValueError:
pass
else:
ascii_art_canvas.draw_point(x_canvas_coord, y_canvas_coord, ".")
30 changes: 21 additions & 9 deletions henon2midi/data_point_to_midi_conversion.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ def create_midi_messages_from_data_point(
clip: bool = False,
x_midi_parameter_mappings: set[str] = {"note"},
y_midi_parameter_mappings: set[str] = {"velocity", "pan"},
source_range_x: tuple[float, float] = (-1.0, 1.0),
source_range_y: tuple[float, float] = (-1.0, 1.0),
midi_range_x: tuple[int, int] = (0, 127),
midi_range_y: tuple[int, int] = (0, 127),
) -> list[Message]:
x = datapoint[0]
y = datapoint[1]
Expand All @@ -21,16 +25,24 @@ def create_midi_messages_from_data_point(
}

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
if (x > source_range_x[1] or x < source_range_x[0]) and not clip:
midi_values["velocity"] = 0
else:
midi_values[x_midi_parameter_mapping] = midi_value_from_data_point_value(x)
midi_values[x_midi_parameter_mapping] = midi_value_from_data_point_value(
x,
source_range=source_range_x,
midi_range=midi_range_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
if (y > source_range_y[1] or y < source_range_y[0]) and not clip:
midi_values["velocity"] = 0
else:
midi_values[y_midi_parameter_mapping] = midi_value_from_data_point_value(y)
midi_values[y_midi_parameter_mapping] = midi_value_from_data_point_value(
y,
source_range=source_range_y,
midi_range=midi_range_y,
)

note_on = Message(
"note_on",
Expand Down Expand Up @@ -85,14 +97,14 @@ def create_midi_messages_from_data_point(

def midi_value_from_data_point_value(
x: float,
data_point_range: tuple[float, float] = (-1.0, 1.0),
source_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]
min_data_point_value = source_range[0]
max_data_point_value = source_range[1]

return round(
rescale_number_to_range(
Expand Down
28 changes: 25 additions & 3 deletions henon2midi/henon_equations.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,19 @@ def __init__(
self.henon_mapping_generator = henon_mapping_generator(
a_parameter, starting_radius, starting_radius
)
self.data_point_generator = self.radially_expending_henon_mappings_generator()
self.data_point_generator = self._radially_expanding_henon_mappings_generator()
self.current_orbital_iteration = 0
self.current_iteration = 0
self.iteration_of_current_orbit = 0
self.times_reset = 0
self.current_radius = starting_radius
self.current_data_point = (starting_radius, starting_radius)

def generate_next_data_point(self) -> tuple[float, float]:
"""
Returns the next data point in the sequence.
If the sequence has reached the end, it will restart the sequence and return the first data point.
"""
try:
data_point = next(self.data_point_generator)
except StopIteration:
Expand All @@ -70,12 +75,20 @@ def generate_next_data_point(self) -> tuple[float, float]:
return data_point

def restart_data_point_generator(self):
self.data_point_generator = self.radially_expending_henon_mappings_generator()
"""
Creates a new data point generator. This is useful if you want to restart the sequence.
"""
self.times_reset += 1
self.data_point_generator = self._radially_expanding_henon_mappings_generator()

def radially_expending_henon_mappings_generator(
def _radially_expanding_henon_mappings_generator(
self,
) -> Generator[tuple[float, float], None, None]:
self._reset_to_starting_radius()
self.current_iteration = 0
self.current_orbital_iteration = 0
self.iteration_of_current_orbit = 0

while self.current_radius <= 1:
self.iteration_of_current_orbit = 0
self.current_orbital_iteration += 1
Expand Down Expand Up @@ -117,3 +130,12 @@ def get_iteration_of_current_orbit(self) -> int:

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

def get_times_reset(self) -> int:
return self.times_reset

def __iter__(self):
return self

def __next__(self):
return next(self.data_point_generator)
Loading

0 comments on commit c5ddff5

Please sign in to comment.