| Music-Gestalt documentation | Contained in the Music-Gestalt distribution. |
Music::Gestalt - Compose music using gestalts.
Version 0.06
use Music::Gestalt;
# see below
This module helps to compose music using musical gestalts (forms). A gestalt is similar to a list in MIDI::Score format, but so far it only supports note events, and all parameters are expressed as values between 0 and 1. This allows for more flexible transformations of the musical material.
newmy $g = Music::Gestalt->new(score => $score);
Creates a new Music::Gestalt object. The optional score argument receives a score in MIDI::Score format.
PitchLowestSets (if you pass a value) and returns lowest pitch used in this gestalt.
PitchHighestSets (if you pass a value) and returns highest pitch used in this gestalt.
PitchMiddleSets (if you pass a value) and returns middle pitch of the range used in this gestalt.
PitchRangeReturns the pitch range used in this gestalt, ie. the pitches that occur will be between pitch middle +/- pitch range.
If you pass a value as parameter, the new pitch range will be calculated around the current pitch middle.
VelocityLowestSets (if you pass a value) and returns lowest velocity used in this gestalt.
VelocityHighestSets (if you pass a value) and returns highest velocity used in this gestalt.
VelocityMiddleSets (if you pass a value) and returns middle velocity of the range used in this gestalt.
VelocityRangeReturns the velocity range used in this gestalt, ie. the velocities that occur will be between velocity middle +/- velocity range.
If you pass a value as parameter, the new velocity range will be calculated around the current velocity middle.
DurationReturns the duration of this gestalt.
If you pass a value as a parameter, it will be used as new duration.
NoteLengthReturns the note length property of this gestalt. "1" means that the original note lengths are retained, ".5" means that each note is half as long as it originally was.
If you pass a value as a parameter, it will be used as new note length.
NotesReturns the note list of this gestalt.
DensitySets and returns the density of this gestalt. The initial density is 1, meaning that all notes will be used. At density 0, a score created from this gestalt will have no events. At density 0.5, a score created from this gestalt will have half the original events, selected at random.
It is intentional that the density may not be greater than 1; there are too many possible ways to generate additional events.
PitchesReturns or sets the pitches used in this gestalt. You should pass areference to a list that contains the pitch numbers used. Returns a reference to a list of pitches that will be used. An empty list signifies that all pitches are used. Note that the set of pitches used is also influenced by the "PitchesMode" parameter.
PitchesModeReturns or sets the pitch mode used in this gestalt.
AsScoreReturns a structure representing the gestalt in MIDI::Score format.
AppendAppends other Music::Gestalt objects to this object.
InsertInserts other Music::Gestalt objects into this object.
MirrorTime, MirrorPitch, MirrorVelocityMirrors the start times, pitches and velocites found in the gestalt.
Using automation, you can vary a parameter over time (comparable to parameter automation in sequencers like Logic or Cubase). For each parameter that can be automated, there are three modes available:
In this mode, the currently set parameter values will be replaced with the values that you pass to the function. If you pass (0, 100), the parameter will range from 0 to 100 over the whole gestalt. If you pass (20, 50 20), the parameter will first rise from 20 to 50, then in the second half of the gestalt fall from 50 to 20. The parameter value is interpolated at the event's position.
In this mode, the currently set parameter values will be multiplied with the values that you pass to the function. If you pass (0, 1), the parameter will fade in over the whole gestalt. If you pass (.2, .5 .2), the parameter will first fade in from 20% to 50%, then in the second half of the gestalt fade out from 50% to 20%. The parameter value is interpolated at the event's position.
Automation will not be applied.
Here are some examples how to call the automation functions:
$g->AutomateVelocityOff(); # default $g->AutomateVelocityAbs(20, 50, 20); $g->AutomateVelocityRel(0, 1);
The values passed to the automation methods will be applied evenly-spaced.
The following automation methods are available:
AutomateVelocityOff, AutomateVelocityAbs, AutomateVelocityRelAutomatePitchMiddleOff, AutomatePitchMiddleAbs, AutomatePitchMiddleRelChristian Renz, <crenz @ web42.com>
Please report any bugs or feature requests to
bug-music-gestalt@rt.cpan.org, or through the web interface at
http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Music-Gestalt.
I will be notified, and then you'll automatically be notified of progress on
your bug as I make changes.
Please also consider adding a test case to your bug report (.t script).
Copyright 2005 Christian Renz, <crenz @ web42.com>, All Rights Reserved.
This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
| Music-Gestalt documentation | Contained in the Music-Gestalt distribution. |
package Music::Gestalt; use warnings; use strict; use fields qw (automation density duration notes note_length pitch_base pitch_extent pitches pitches_count pitches_mode pitches_mapping pitches_row_pos velocity_base velocity_extent); use 5.006; use MIDI::Pitch qw(findsemitone);
our $VERSION = '0.06';
sub new { my $class = shift; my %params = @_; my $self = fields::new($class); $self->{pitches_count} = 0; $self->{pitches_mode} = 'all'; $self->{density} = 1; $self->{note_length} = 1; $self->_InitializeFromScore($params{score}) if (ref $params{score} eq 'ARRAY'); $self->_PitchesUpdateMapping(); return $self; } # private methods sub _InitializeFromScore { my ($self, $score) = @_; my ($max_time, $min_pitch, $max_pitch, $min_velocity, $max_velocity); # find min/max values # #0: 'note' # #1: start time # #2: duration # #3: channel # #4: note # #5: velocity foreach (@$score) { next unless $_->[0] eq 'note'; # min_time is always 0 $max_time = $_->[1] + $_->[2] if (!defined $max_time || $_->[1] + $_->[2] > $max_time); $min_pitch = $_->[4] if (!defined $min_pitch || $_->[4] < $min_pitch); $max_pitch = $_->[4] if (!defined $max_pitch || $_->[4] > $max_pitch); $min_velocity = $_->[5] if (!defined $min_velocity || $_->[5] < $min_velocity); $max_velocity = $_->[5] if (!defined $max_velocity || $_->[5] > $max_velocity); } $self->{pitch_base} = $min_pitch; $self->{pitch_extent} = defined $min_pitch && defined $max_pitch ? $max_pitch - $min_pitch : undef; $self->{velocity_base} = $min_velocity; $self->{velocity_extent} = defined $min_velocity && defined $max_velocity ? $max_velocity - $min_velocity : undef; $self->{duration} = $max_time || 0; $self->{notes} = []; return if (!defined $max_time || $max_time == 0); my @notes = (); my $pitch_extent = ($max_pitch - $min_pitch); my $velocity_extent = ($max_velocity - $min_velocity); foreach (@$score) { next unless $_->[0] eq 'note'; push @notes, [ $_->[1] / $max_time, $_->[2] / $max_time, ($_->[3] - 1) / 15, $pitch_extent == 0 ? 0 : ($_->[4] - $min_pitch) / $pitch_extent, $velocity_extent == 0 ? 0 : ($_->[5] - $min_velocity) / $velocity_extent]; } $self->{notes} = [@notes]; } sub _min { my ($a, $b) = @_; return $a < $b ? $a : $b; } sub _max { my ($a, $b) = @_; return $a > $b ? $a : $b; } sub _CalcAutomationValue { my ($self, $param_name, $position, $value) = @_; return $value unless (ref $self->{automation}->{$param_name} eq 'HASH' && ref $self->{automation}->{$param_name}->{values} eq 'ARRAY'); my %am = %{$self->{automation}->{$param_name}}; my $count = scalar @{$am{values}}; if ($count == 1) { if ($am{mode} eq 'absolute') { return $am{values}->[0]; } else { return $value * $am{values}->[0]; } } my $time_per_value = 1 / ($count - 1); my $div = $position / $time_per_value; my $idx = int($div); my $frac = $div - $idx; my $base = $am{values}->[$idx]; my $extent = $am{values}->[$idx + 1] - $base; my $val = $base + $frac * $extent; if ($am{mode} eq 'absolute') { return $val; } else { return $value * $val; } } sub _CalcPitch { my ($self, $v, $pos) = @_; my $pb = $self->{pitch_base}; my $pe = $self->{pitch_extent}; if (ref $self->{automation}->{pitch_middle}->{values} eq 'ARRAY') { my $pm = $self->PitchMiddle(); $pb += $self->_CalcAutomationValue('pitch_middle', $pos, $pm) - $pm; } my $val = $pb + $v * $pe + 0.5; $val = _max(0, _min(int($val), 127)); my $pitch = $self->{pitches_mapping}->{$val}; if ($self->{pitches_mode} eq 'row' && ref $self->{pitches} eq 'ARRAY') { $pitch = findsemitone($self->{pitches}->[$self->{pitches_row_pos}], $pitch); $self->{pitches_row_pos} = ($self->{pitches_row_pos} + 1) % scalar @{$self->{pitches}}; } return $pitch; } sub _CalcVelocity { my ($self, $v, $pos) = @_; my $val = $self->_CalcAutomationValue('velocity', $pos, $self->{velocity_base} + $v * $self->{velocity_extent} + 0.5); return _max(0, _min(int($val), 127)); }
sub PitchLowest { my ($self, $v) = @_; if (defined $v) { $v = _max(0, _min($self->{pitch_base} + $self->{pitch_extent}, $v)); $self->{pitch_extent} = $self->{pitch_base} + $self->{pitch_extent} - $v; $self->{pitch_base} = $v; } return $self->{pitch_base}; }
sub PitchHighest { my ($self, $v) = @_; return undef unless defined $self->{pitch_base} && defined $self->{pitch_extent}; if (defined $v) { $v = _min(127, _max($self->{pitch_base}, $v)); $self->{pitch_extent} = $v - $self->{pitch_base}; } return $self->{pitch_base} + $self->{pitch_extent}; }
sub PitchMiddle { my ($self, $v) = @_; return undef unless defined $self->{pitch_base} && defined $self->{pitch_extent}; if (defined $v) { $v = _min(_max($v, 0), 127); my $pm_old = (2 * $self->{pitch_base} + $self->{pitch_extent}) / 2; $self->{pitch_base} += $v - $pm_old; } return (2 * $self->{pitch_base} + $self->{pitch_extent}) / 2; }
sub PitchRange { my ($self, $value) = @_; return undef unless defined $self->{pitch_base}; if (defined $value) { my $old_range = $self->{pitch_extent} / 2; $self->{pitch_extent} = 2 * $value; $self->{pitch_base} += $old_range - $value; } return undef unless defined $self->{pitch_extent}; return $self->{pitch_extent} / 2; }
sub VelocityLowest { my ($self, $v) = @_; if (defined $v) { $v = _max(0, _min($self->{velocity_base} + $self->{velocity_extent}, $v)); $self->{velocity_extent} = $self->{velocity_base} + $self->{velocity_extent} - $v; $self->{velocity_base} = $v; } return $self->{velocity_base}; }
sub VelocityHighest { my ($self, $v) = @_; return undef unless defined $self->{velocity_base} && defined $self->{velocity_extent}; if (defined $v) { $v = _min(127, _max($self->{velocity_base}, $v)); $self->{velocity_extent} = $v - $self->{velocity_base}; } return $self->{velocity_base} + $self->{velocity_extent}; }
sub VelocityMiddle { my ($self, $v) = @_; return undef unless defined $self->{velocity_base} && defined $self->{velocity_extent}; if (defined $v) { $v = _min(_max($v, 0), 127); my $vm_old = (2 * $self->{velocity_base} + $self->{velocity_extent}) / 2; $self->{velocity_base} += $v - $vm_old; } return (2 * $self->{velocity_base} + $self->{velocity_extent}) / 2; }
sub VelocityRange { my ($self, $value) = @_; return undef unless defined $self->{velocity_base}; if (defined $value) { my $old_range = $self->{velocity_extent} / 2; $self->{velocity_extent} = 2 * $value; $self->{velocity_base} += $old_range - $value; } return undef unless defined $self->{velocity_extent}; return $self->{velocity_extent} / 2; }
sub Duration { my ($self, $v) = @_; $self->{duration} = _max(0, $v) if defined $v; return $self->{duration} || 0; }
sub NoteLength { my ($self, $v) = @_; $self->{note_length} = _max(0, $v) if defined $v; return $self->{note_length} || 0; }
sub Notes { my $self = shift; return @{[]} unless ref $self->{notes} eq 'ARRAY'; return @{$self->{notes}}; }
sub Density { my ($self, $v) = @_; $self->{density} = _min(1, _max(0, $v)) if defined $v; return $self->{density}; }
sub Pitches { my ($self, $pitches) = @_; if (defined $pitches && ref $pitches eq 'ARRAY') { $self->{pitches} = [map { _max(0, _min($_, 127)) } @$pitches]; $self->{pitches_count} = scalar @$pitches; $self->_PitchesUpdateMapping(); } if (defined $self->{pitches}) { return $pitches; } else { return []; } }
sub _PitchesUpdateMapping { my ($self) = @_; if (ref $self->{pitches} eq 'ARRAY' && scalar @{$self->{pitches}} > 0) { my %map; if ($self->{pitches_mode} eq 'nearest') { if (scalar @{$self->{pitches}} == 1) { %map = map { $_ => $self->{pitches}->[0] } 0 .. 127; return $self->{pitches_mapping} = {%map}; } my @list = sort { $a <=> $b } @{$self->{pitches}}; my $idx = 0; for (0 .. 127) { my $diffA = abs($list[$idx] - $_); my $diffB = abs($list[$idx + 1] - $_); if ($diffA < $diffB) { $map{$_} = $list[$idx]; } else { $map{$_} = $list[$idx + 1]; $idx++ if ($idx < $#list); } } return $self->{pitches_mapping} = {%map}; } } $self->{pitches_mapping} = {map { $_ => $_ } 0 .. 127}; } sub PitchesMode { my ($self, $mode) = @_; if (defined $mode) { $self->{pitches_mode} = $mode; $self->_PitchesUpdateMapping(); } return $self->{pitches_mode}; }
sub AsScore { my $self = shift; return [] if $self->{density} == 0; my @score = (); $self->{pitches_row_pos} = 0; foreach (@{$self->{notes}}) { push @score, [ 'note', $_->[0] * $self->{duration}, $_->[1] * $self->{duration} * $self->{note_length}, int(($_->[2] * 15 + 0.5) + 1), $self->_CalcPitch($_->[3], $_->[0]), $self->_CalcVelocity($_->[4], $_->[0])]; } # remove events if density < 1 if ($self->{density} < 1) { for (1 .. int((1 - $self->{density}) * scalar @score)) { splice @score, int(rand(scalar @score)), 1; } } return [@score]; }
sub Append { my $self = shift; # 1. Find out lowest/highest pitch and velocity overall my $pitch_lowest = $self->PitchLowest(); my $pitch_highest = $self->PitchHighest(); my $velocity_lowest = $self->VelocityLowest(); my $velocity_highest = $self->VelocityHighest(); my $duration = $self->Duration() || 0; @_ = grep { UNIVERSAL::isa($_, 'Music::Gestalt') } @_; foreach (@_) { $pitch_lowest = $_->PitchLowest() if (!defined $pitch_lowest || $_->PitchLowest() < $pitch_lowest); $pitch_highest = $_->PitchHighest() if (!defined $pitch_highest || $_->PitchHighest() > $pitch_highest); $velocity_lowest = $_->VelocityLowest() if (!defined $velocity_lowest || $_->VelocityLowest() < $velocity_lowest); $velocity_highest = $_->VelocityHighest() if (!defined $velocity_highest || $_->VelocityHighest() > $velocity_highest); $duration += $_->Duration; } return if ( !defined $pitch_lowest || !defined $pitch_highest || !defined $velocity_lowest || !defined $velocity_highest); my $pitch_extent = $pitch_highest - $pitch_lowest; my $velocity_extent = $velocity_highest - $velocity_lowest; # 2. Transform notes in this gestalt to new pitch foreach (@{$self->{notes}}) { # start time, duration, pitch, velocity $_->[0] = $_->[0] * $self->{duration} / $duration; $_->[1] = $_->[1] * $self->{duration} / $duration; # channel stays as it is $_->[3] = $pitch_extent == 0 ? 0 : ( ( $self->{pitch_base} - $pitch_lowest + $self->{pitch_extent} * $_->[3]) / $pitch_extent); $_->[4] = $velocity_extent == 0 ? 0 : ( ( $self->{velocity_base} - $velocity_lowest + $self->{velocity_extent} * $_->[4]) / $velocity_extent); } # 3. Transform and append notes in the gestalts to be appended my $time_pos = $self->{duration} || 0; foreach my $g (@_) { my $time_delta = $time_pos / $duration; my $gpitchextent = $g->PitchHighest() - $g->PitchLowest(); my $gvelocityextent = $g->VelocityHighest() - $g->VelocityLowest(); my $dur = $g->Duration() / $duration; foreach ($g->Notes()) { push @{$self->{notes}}, [ $time_delta + ($_->[0] * $dur), $_->[1] * $dur, $_->[2], $pitch_extent == 0 ? 0 : ( ( $g->PitchLowest() - $pitch_lowest + $gpitchextent * $_->[3]) / $pitch_extent), $velocity_extent == 0 ? 0 : ( ( $g->VelocityLowest() - $velocity_lowest + $gvelocityextent * $_->[4]) / $velocity_extent)]; } $time_pos += $g->Duration(); } # 4. Save new attributes in this gestalt $self->{pitch_base} = $pitch_lowest; $self->{pitch_extent} = $pitch_extent; $self->{velocity_base} = $velocity_lowest; $self->{velocity_extent} = $velocity_extent; $self->{duration} = $duration; }
sub Insert { my $self = shift; # 1. Find out lowest/highest pitch and velocity overall my $pitch_lowest = $self->PitchLowest(); my $pitch_highest = $self->PitchHighest(); my $velocity_lowest = $self->VelocityLowest(); my $velocity_highest = $self->VelocityHighest(); my $duration = $self->Duration() || 0; @_ = grep { UNIVERSAL::isa($_, 'Music::Gestalt') } @_; foreach (@_) { $pitch_lowest = $_->PitchLowest() if (!defined $pitch_lowest || $_->PitchLowest() < $pitch_lowest); $pitch_highest = $_->PitchHighest() if (!defined $pitch_highest || $_->PitchHighest() > $pitch_highest); $velocity_lowest = $_->VelocityLowest() if (!defined $velocity_lowest || $_->VelocityLowest() < $velocity_lowest); $velocity_highest = $_->VelocityHighest() if (!defined $velocity_highest || $_->VelocityHighest() > $velocity_highest); $duration = $_->Duration() if $_->Duration() > $duration; } return if ( !defined $pitch_lowest || !defined $pitch_highest || !defined $velocity_lowest || !defined $velocity_highest); my $pitch_extent = $pitch_highest - $pitch_lowest; my $velocity_extent = $velocity_highest - $velocity_lowest; # 2. Transform notes in this gestalt to new pitch foreach (@{$self->{notes}}) { # start time, duration, pitch, velocity $_->[0] = $_->[0] * $self->{duration} / $duration; $_->[1] = $_->[1] * $self->{duration} / $duration; # channel stays as it is $_->[3] = $pitch_extent == 0 ? 0 : ( ( $self->{pitch_base} - $pitch_lowest + $self->{pitch_extent} * $_->[3]) / $pitch_extent); $_->[4] = $velocity_extent == 0 ? 0 : ( ( $self->{velocity_base} - $velocity_lowest + $self->{velocity_extent} * $_->[4]) / $velocity_extent); } # 3. Transform and insert notes in the gestalts to be inserted foreach my $g (@_) { my $gpitchextent = $g->PitchHighest() - $g->PitchLowest(); my $gvelocityextent = $g->VelocityHighest() - $g->VelocityLowest(); my $notes_pos = 0; my $dur = $g->Duration() / $duration; foreach ($g->Notes()) { my $start = $_->[0] * $dur; $notes_pos++ while ($notes_pos <= $#{$self->{notes}} && $self->{notes}->[$notes_pos]->[0] <= $start); splice @{$self->{notes}}, $notes_pos, 0, [ $_->[0] * $dur, $_->[1] * $dur, $_->[2], $pitch_extent == 0 ? 0 : ( ( $g->PitchLowest() - $pitch_lowest + $gpitchextent * $_->[3]) / $pitch_extent), $velocity_extent == 0 ? 0 : ( ( $g->VelocityLowest() - $velocity_lowest + $gvelocityextent * $_->[4]) / $velocity_extent)]; } } # 4. Save new attributes in this gestalt $self->{pitch_base} = $pitch_lowest; $self->{pitch_extent} = $pitch_extent; $self->{velocity_base} = $velocity_lowest; $self->{velocity_extent} = $velocity_extent; $self->{duration} = $duration; }
sub _mirror { my ($self, $param) = @_; return unless ref $self->{notes} eq 'ARRAY'; foreach (@{$self->{notes}}) { # die "\n\n\nparam = $param\n\n\n"; # start time, duration, pitch, velocity $_->[$param] = 1 - $_->[$param]; } } sub MirrorTime { my ($self) = @_; $self->_mirror(0); } sub MirrorPitch { my ($self) = @_; $self->_mirror(3); } sub MirrorVelocity { my ($self) = @_; $self->_mirror(4); }
sub AutomateVelocityOff { my $self = shift; delete $self->{automation}->{velocity}; } sub AutomateVelocityAbs { my $self = shift; if (scalar @_) { $self->{automation}->{velocity}->{mode} = 'absolute'; $self->{automation}->{velocity}->{values} = [map { _min(127, _max(0, $_)) } @_]; } return $self->{automation}->{velocity}; } sub AutomateVelocityRel { my $self = shift; if (scalar @_) { $self->{automation}->{velocity}->{mode} = 'relative'; $self->{automation}->{velocity}->{values} = [@_]; } return $self->{automation}->{velocity}; }
sub AutomatePitchMiddleOff { my $self = shift; delete $self->{automation}->{pitch_middle}; } sub AutomatePitchMiddleAbs { my $self = shift; if (scalar @_) { $self->{automation}->{pitch_middle}->{mode} = 'absolute'; $self->{automation}->{pitch_middle}->{values} = [map { _min(127, _max(0, $_)) } @_]; } return $self->{automation}->{pitch_middle}; } sub AutomatePitchMiddleRel { my $self = shift; if (scalar @_) { $self->{automation}->{pitch_middle}->{mode} = 'relative'; $self->{automation}->{pitch_middle}->{values} = [@_]; } return $self->{automation}->{pitch_middle}; }
42;