def __init__(self, MMVVectorial, context, skia_object):
        self.vectorial = MMVVectorial
        self.context = context
        self.skia = skia_object
        self.config = self.vectorial.config
        self.polar = PolarCoordinates()
        self.functions = Functions()

        self.center_x = self.vectorial.context.width / 2
        self.center_y = self.vectorial.context.height / 2
    def __init__(self, MMVVectorial, context, skia_object, midi):
        self.vectorial = MMVVectorial
        self.context = context
        self.skia = skia_object
        self.midi = midi
        self.config = self.vectorial.config
        self.functions = Functions()
        self.datautils = DataUtils()
        self.piano_keys = {}
        self.keys_centers = {}

        self.background_color = 22 / 255
예제 #3
0
    def __init__(self, mmv, MMVSkiaPianoRollVectorial):
        self.mmvskia_main = mmv
        self.vectorial = MMVSkiaPianoRollVectorial
        self.config = self.vectorial.config
        self.functions = Functions()
        self.datautils = DataUtils()
        self.piano_keys = {}
        self.keys_centers = {}
        """
        When converting the colors we divide by 255 so we normalize in a value in between 0 and 1
        Because skia works like that.
        """

        sep = os.path.sep

        # Load the yaml config file
        color_preset = self.config["color_preset"]
        color_config_yaml = self.mmvskia_main.utils.load_yaml(
            f"{self.mmvskia_main.mmvskia_interface.top_level_interace.data_dir}{sep}mmvskia{sep}piano_roll{sep}color_{color_preset}.yaml"
        )

        # Get the global colors into a dictionary

        self.global_colors = {}

        for key in color_config_yaml["global"]:
            self.global_colors[key] = [
                channel / 255 for channel in ImageColor.getcolor(
                    "#" + str(color_config_yaml["global"][key]), "RGB")
            ]

        # # Get the note colors based on their channel

        self.color_channels = {}

        # For every channel config
        for key in color_config_yaml["channels"]:

            # Create empty dir
            self.color_channels[key] = {}

            # Get colors of sharp and plain, border, etc..
            for color_type in color_config_yaml["channels"][key].keys():

                # Hexadecimal representation of color
                color_hex = color_config_yaml["channels"][key][color_type]

                # Assign RGB value
                self.color_channels[key][color_type] = [
                    channel / 255
                    for channel in ImageColor.getcolor(f"#{color_hex}", "RGB")
                ]
예제 #4
0
    def __init__(self) -> None:
        debug_prefix = "[AudioProcessing.__init__]"

        # Create some util classes
        self.fourier = Fourier()
        self.datautils = DataUtils()
        self.functions = Functions()
        self.config = None

        # List of full frequencies of notes
        # - 50 to 68 yields freqs of 24.4 Hz up to
        self.piano_keys_frequencies = [
            round(self.get_frequency_of_key(x), 2) for x in range(-50, 68)
        ]
    def __init__(self, mmv, MMVSkiaPianoRollVectorial):
        self.mmvskia_main = mmv
        self.vectorial = MMVSkiaPianoRollVectorial
        self.config = self.vectorial.config
        self.functions = Functions()
        self.datautils = DataUtils()
        self.piano_keys = {}
        self.keys_centers = {}
        self.colors = {}

        # Generate skia.Color4F out of the colors dictionary
        self.colors = self.parse_colors_dict(colors_dict = self.config["colors"])

        self.font_fits_on_width = {}
예제 #6
0
class Interpolation:

    def __init__(self):
        self.functions = Functions()

    # Linear, between point A and B based on a current "step" and total steps
    def linear(self,
            start_value: Number,
            target_value: Number,
            current_step: Number,
            total_steps: Number,
        ) -> Number:

        if current_step > total_steps:
            return target_value

        part = (target_value - start_value) / total_steps
        walked = part * current_step
        return start_value + walked

    # "Biased" remaining linear
    # aggressive, 0.05 is smooth, 0.1 is medium, 1 is instant
    # random is a decimal that adds to aggressive randomly
    def remaining_approach(self,
            start_value: Number,
            target_value: Number,
            current_step: Number,
            current_value: Number,
            aggressive: Number,
            aggressive_randomness: Number=0,
        ) -> Number:

        # We're at the first step, so start on current value
        if current_step == 0:
            return start_value

        return current_value + ( (target_value - current_value) * (aggressive + random.uniform(0, aggressive_randomness)) )

    # Sigmoid activation between two points, smoothed out "linear" curver
    def sigmoid(self,
            start_value: Number,
            target_value: Number,
            smooth: Number,
        ) -> Number:

        distance = (target_value - start_value)
        where = self.functions.proportion(total, 1, current)
        walk = distance * self.functions.sigmoid(where, smooth)
        return a + walk
예제 #7
0
    def __init__(self, context, config: dict, skia_object) -> None:
        
        debug_prefix = "[MMVMusicBars.__init__]"
        
        self.context = context
        self.config = config
        self.skia = skia_object

        self.fit_transform_index = FitIndex()
        self.functions = Functions()
        self.utils = Utils()

        self.path = {}

        self.x = 0
        self.y = 0
        self.size = 1
        self.is_deletable = False
        self.offset = [0, 0]
        self.polar = PolarCoordinates()

        self.current_fft = {}

        self.image = Frame()

        # We use separate file and classes for each type of visualizer

        # Circle, radial visualizer
        if self.config["type"] == "circle":
            self.builder = MMVMusicBarsCircle(self, self.context, self.skia)
예제 #8
0
    def __init__(self, depth = LOG_NO_DEPTH) -> None:
        debug_prefix = "[AudioProcessing.__init__]"
        ndepth = depth + LOG_NEXT_DEPTH

        self.fourier = Fourier()
        self.datautils = DataUtils()
        self.functions = Functions()
        self.config = None

        # MMV specific, where we return repeated frequencies from the
        # function process
        self.where_decay_less_than_one = 440
        self.value_at_zero = 5

        # List of full frequencies of notes
        # - 50 to 68 yields freqs of 24.4 Hz up to 
        self.piano_keys_frequencies = [round(self.get_frequency_of_key(x), 2) for x in range(-50, 68)]
        logging.info(f"{depth}{debug_prefix} Whole notes frequencies we'll care: [{self.piano_keys_frequencies}]")
예제 #9
0
    def __init__(self, MMVSkiaVectorial, mmv):
        self.mmv = mmv
        self.vectorial = MMVSkiaVectorial
        self.config = self.vectorial.config
        self.polar = PolarCoordinates()
        self.functions = Functions()

        # Leave this much of blank space at the left and right edge
        bleed_x = self.mmv.context.width * (1 / 19)

        # Where Y starts and ends, ie. what horizontal line to draw the progression bar on
        if self.config["position"] == "bottom":
            start_end_y = (19 / 20) * self.mmv.context.height

        elif self.config["position"] == "top":
            start_end_y = (1 / 20) * self.mmv.context.height

        # End X we have to count the bleed
        self.start_x = bleed_x
        self.end_x = self.mmv.context.width - bleed_x

        self.start_y = start_end_y
        self.end_y = start_end_y
예제 #10
0
    def setup(self) -> None:
        debug_prefix = "[MMVSkiaMain.setup]"

        self.utils = Utils()

        logging.info(f"{debug_prefix} Creating MMVContext() class")
        self.context = MMVContext(mmv_skia_main=self)

        logging.info(f"{debug_prefix} Creating SkiaNoWindowBackend() class")
        self.skia = SkiaNoWindowBackend()

        logging.info(f"{debug_prefix} Creating Functions() class")
        self.functions = Functions()

        logging.info(f"{debug_prefix} Creating Interpolation() class")
        self.interpolation = Interpolation()

        logging.info(f"{debug_prefix} Creating PolarCoordinates() class")
        self.polar_coordinates = PolarCoordinates()

        logging.info(f"{debug_prefix} Creating Canvas() class")
        self.canvas = MMVSkiaImage(mmvskia_main=self)

        logging.info(f"{debug_prefix} Creating Fourier() class")
        self.fourier = Fourier()

        # The user must explicitly set and override this, mostly for compatibility
        # and code cleanup reasons.
        self.pipe_video_to = None

        logging.info(f"{debug_prefix} Creating AudioFile() class")
        self.audio = AudioFile()

        logging.info(f"{debug_prefix} Creating AudioProcessing() class")
        self.audio_processing = AudioProcessing()

        logging.info(f"{debug_prefix} Creating MMVSkiaAnimation() class")
        self.mmv_skia_animation = MMVSkiaAnimation(mmv_skia_main=self)

        logging.info(f"{debug_prefix} Creating MMVSkiaCore() class")
        self.core = MMVSkiaCore(mmvskia_main=self)
예제 #11
0
    def __init__(self, mmv, **kwargs) -> None:
        self.mmv = mmv

        debug_prefix = "[MMVSkiaMusicBars.__init__]"

        self.kwargs = kwargs

        self.functions = Functions()
        self.utils = Utils()

        self.path = {}

        self.x = 0
        self.y = 0
        self.size = 1
        self.is_deletable = False
        self.offset = [0, 0]
        self.polar = PolarCoordinates()

        self.current_fft = {}

        self.image = Frame()

        # Configuration

        self.kwargs["fourier"] = {
            "interpolation":
            MMVSkiaInterpolation(
                self.mmv,
                function="remaining_approach",
                ratio=self.kwargs.get("bar_responsiveness", 0.25),
            ),
        }

        # # We use separate file and classes for each type of visualizer

        # Circle, radial visualizer
        if self.kwargs["type"] == "circle":
            self.builder = MMVSkiaMusicBarsCircle(self.mmv, self,
                                                  **self.kwargs)
예제 #12
0
    def setup(self) -> None:

        debug_prefix = "[MMVMain.__init__]"

        self.utils = Utils()

        print(debug_prefix, "Creating Context()")
        self.context = Context(self)

        print(debug_prefix, "Creating SkiaNoWindowBackend()")
        self.skia = SkiaNoWindowBackend(self)

        print(debug_prefix, "Creating Functions()")
        self.functions = Functions()

        print(debug_prefix, "Creating Interpolation()")
        self.interpolation = Interpolation()

        print(debug_prefix, "Creating Canvas()")
        self.canvas = MMVImage(self)

        print(debug_prefix, "Creating Fourier()")
        self.fourier = Fourier()

        print(debug_prefix, "Creating FFmpegWrapper()")
        self.ffmpeg = FFmpegWrapper(self)

        print(debug_prefix, "Creating AudioFile()")
        self.audio = AudioFile()

        print(debug_prefix, "Creating AudioProcessing()")
        self.audio_processing = AudioProcessing()

        print(debug_prefix, "Creating MMVAnimation()")
        self.mmv_animation = MMVAnimation(self)
    
        print(debug_prefix, "Creating Core()")
        self.core = Core(self)
class MMVSkiaPianoRollTopDown:
    def __init__(self, mmv, MMVSkiaPianoRollVectorial):
        self.mmvskia_main = mmv
        self.vectorial = MMVSkiaPianoRollVectorial
        self.config = self.vectorial.config
        self.functions = Functions()
        self.datautils = DataUtils()
        self.piano_keys = {}
        self.keys_centers = {}
        self.colors = {}

        # Generate skia.Color4F out of the colors dictionary
        self.colors = self.parse_colors_dict(colors_dict = self.config["colors"])

        self.font_fits_on_width = {}

    # Recursively parse a colors dict
    def parse_colors_dict(self, colors_dict):
        for key, value in colors_dict.items():
            if value is None:
                colors_dict[key] = None
                continue 
            
            if isinstance(value, dict):
                colors_dict[key] = self.parse_colors_dict(colors_dict = value)
                continue

            if isinstance(value[0], int):
                value = [v / 256 for v in value]

            colors_dict[key] = skia.Color4f(*value)
        return colors_dict

    # Bleed is extra keys you put at the lower most and upper most ranged keys
    def generate_piano(self, min_note, max_note):

        debug_prefix = "[MMVSkiaPianoRollTopDown.generate_piano]"

        # NOTE: EDIT HERE STATIC VARIABLES  TODO: set them on config
        self.piano_height = (3.5/19) * self.mmvskia_main.context.height
        self.viewport_height = self.mmvskia_main.context.height - self.piano_height

        # Pretty print
        print("Add key by index: {", end = "", flush = True)

        # For each key index, with a bleed (extra keys) counting up and down from the minimum / maximum key index
        for key_index in range(min_note - self.config["bleed"], max_note + self.config["bleed"]):

            # Create a PianoKey object and set its index
            next_key = PianoKey(self.mmvskia_main, self.vectorial, self)
            next_key.by_key_index(key_index)

            # Pretty print
            if not key_index == max_note + self.config["bleed"] - 1:
                print(", ", end = "", flush = True)
            else:
                print("}")

            # Add to the piano keys list
            self.piano_keys[key_index] = next_key

        print(debug_prefix, "There will be", len(self.piano_keys.keys()), "keys in the piano roll")
        
        # We get the center of keys based on the "distance walked" in between intervals
        # As black keys are in between two white keys, we walk half white key width on those
        # and a full white key between E and F, B and C.

        # Get number of semitones (divisions) so we can calculate the semitone width afterwards
        divisions = 0

        # For each index and key index (you'll se in a while why)
        for index, key_index in enumerate(self.piano_keys.keys()):

            # Get this key object
            key = self.piano_keys[key_index]
            
            # First index of key can't compare to previous key, starts at current_center
            if not index == 0:

                # The previous key can be a white or black key, but there isn't a previous at index=0
                prevkey = self.piano_keys[key_index - 1]

                # Both are True, add two, one is True, add one
                # 2 is a tone distance
                # 1 is a semitone distance
                # [True is 1 in Python]
                divisions += (prevkey.is_white) + (key.is_white)

        # The keys intervals for walking
        self.semitone_width = self.mmvskia_main.context.width / divisions
        self.tone_width = self.semitone_width * 2

        self.set_best_font()

        # Current center we're at on the X axis so we send to the keys their positions
        current_center = 0

        # Same loop, ignore index=0
        for index, key_index in enumerate(self.piano_keys.keys()):

            # Get this key object
            key = self.piano_keys[key_index]
            
            # First index of key can't compare to previous key, starts at current_center
            if not index == 0:

                prevkey = self.piano_keys[key_index - 1]

                # Distance is a tone
                if (prevkey.is_white) and (key.is_white):
                    current_center += self.tone_width

                # Distance is a semitone
                else:
                    current_center += self.semitone_width

            # Set the note length according to if it's white or black
            if key.is_white:
                this_note_width = self.tone_width
            else:
                this_note_width = (4/6) * self.tone_width

            # Set attributes to this note we're looping
            self.piano_keys[key_index].width = this_note_width
            self.piano_keys[key_index].height = self.piano_height
            self.piano_keys[key_index].resolution_height = self.mmvskia_main.context.height
            self.piano_keys[key_index].center_x = current_center

            # And add to the key centers list this key center_x
            self.keys_centers[key_index] = current_center

    # Draw the piano, first draw the white then the black otherwise we have overlaps
    def draw_piano(self):
        
        end_piano_pos = self.mmvskia_main.context.height - self.piano_height
        screen_coords = [-200, end_piano_pos, self.mmvskia_main.context.width + 200, end_piano_pos]

        # Make the skia Paint and
        screen_paint = skia.Paint(
            AntiAlias = True,
            Style = skia.Paint.kStroke_Style,
            ImageFilter = skia.ImageFilters.DropShadowOnly(0, 0, 12, 12, self.colors["end_piano_shadow"]),
            StrokeWidth = 20
        )

        self.mmvskia_main.skia.canvas.drawRect(skia.Rect(*screen_coords), screen_paint)

        # White keys
        for key_index in self.piano_keys.keys():
            if self.piano_keys[key_index].is_white:
                self.piano_keys[key_index].draw(self.notes_playing)

        # Black keys
        for key_index in self.piano_keys.keys():
            if self.piano_keys[key_index].is_black:
                self.piano_keys[key_index].draw(self.notes_playing)
        

    # Draw the markers IN BETWEEN TWO WHITE KEYS
    def draw_markers(self):

        # The paint of markers in between white keys
        white_white_paint = skia.Paint(
            AntiAlias = True,
            Color = self.colors["marker_color_between_two_white"],
            Style = skia.Paint.kStroke_Style,
            StrokeWidth = 1,
        )

        current_center = 0

        # Check if prev and this key is white, keeps track of a current_center, create rect to draw and draw
        for index, key_index in enumerate(self.piano_keys.keys()):
            key = self.piano_keys[key_index]
            if not index == 0:
                prevkey = self.piano_keys[key_index - 1]
                if (prevkey.is_white) and (key.is_white):
                    current_center += self.tone_width
                    rect = skia.Rect(
                        current_center - self.semitone_width,
                        0,
                        current_center - self.semitone_width,
                        self.mmvskia_main.context.height
                    )
                    self.mmvskia_main.skia.canvas.drawRect(rect, white_white_paint)
                else:
                    current_center += self.semitone_width

        # Ask each key to draw their CENTERED marker
        for key_index in self.piano_keys.keys():
            self.piano_keys[key_index].draw_marker()

    # Draw a given note according to the seconds of midi content on the screen,
    # horizontal (note), vertical (start / end time in seconds) and color (channel)
    def draw_note(self, velocity, start, end, channel, note, name):

        # Get the note colors for this channel, we receive a dict with "sharp" and "plain" keys
        note_colors = self.colors[f"channel_{channel}"]
        
        # Is a sharp key
        if "#" in name:
            width = self.semitone_width * 0.9
            color1 = note_colors["sharp_1"]
            color2 = note_colors.get("sharp_2", color1)

        # Plain key
        else:
            width = self.tone_width * 0.6
            color1 = note_colors["plain_1"]
            color2 = note_colors.get("plain_2", color1)

        # Make the skia Paint
        note_paint = skia.Paint(
            AntiAlias = True,
            Style = skia.Paint.kFill_Style,
            Shader = skia.GradientShader.MakeLinear(
                points = [(0.0, 0.0), (self.mmvskia_main.context.width, self.mmvskia_main.context.height)],
                colors = [color1, color2]
            ),
            StrokeWidth = 2,
        )

        # Border of the note
        note_border_paint = skia.Paint(
            AntiAlias = True,
            Style = skia.Paint.kStroke_Style,
            ImageFilter = skia.ImageFilters.DropShadowOnly(3, 3, 30, 30, note_colors["border_shadow"]),
            # MaskFilter=skia.MaskFilter.MakeBlur(skia.kNormal_BlurStyle, 2.0),
            # StrokeWidth = max(self.mmvskia_main.context.resolution_ratio_multiplier * 2, 1),
            StrokeWidth = 0,
        )
        
        # Horizontal we have it based on the tones and semitones we calculated previously
        # this is the CENTER of the note
        x = self.keys_centers[note]

        # The Y is a proportion of, if full seconds of midi content, it's the viewport height itself,
        # otherwise it's a proportion to the processing time according to a start value in seconds
        y = self.functions.proportion(
            self.config["seconds_of_midi_content"],
            self.viewport_height, #*2,
            self.mmvskia_main.context.current_time - start
        )

        # The height is just the proportion of, seconds of midi content is the maximum height
        # how much our key length (end - start) is according to that?
        height = self.functions.proportion(
            self.config["seconds_of_midi_content"],
            self.viewport_height,
            end - start
        ) 

        spacing = self.viewport_height / 512

        # Build the coordinates of the note
        # Note: We add and subtract half a width because X is the center
        # while we need to add from the viewport out heights on the Y
        coords = [
            x - (width / 2),
            y + (self.viewport_height) - height + spacing,
            x + (width / 2),
            y + (self.viewport_height) - spacing,
        ]

        # Rectangle border of the note
        # rect = skia.Rect(*coords)
        rect = skia.RRect(skia.Rect(*coords), 
            self.config["rounding"]["notes"]["x"],
            self.config["rounding"]["notes"]["y"]
        )
        
        # Draw the note and border
        self.mmvskia_main.skia.canvas.drawRRect(rect, note_paint)

        # Get paint, font
        if self.config["draw_note_name"]:
            paint = skia.Paint(AntiAlias = True, Color = skia.ColorBLACK, StrokeWidth = 43)

            height_size = self.font.getSpacing()
            text_size = self.font.measureText(name[:-1])

            self.mmvskia_main.skia.canvas.drawString(
                name[:-1],
                (x) - (text_size / 2),
                (y + (self.viewport_height) + spacing) - (height_size / 2),
                self.font, paint
            )

    def set_best_font(self):
        text_fits = 0

        while True:
            text_fits += 1

            font = skia.Font(skia.Typeface('Ubuntu'), text_fits)
            height_size = font.getSpacing()
            text_size = font.measureText("F#")

            if text_size > self.semitone_width:
                break
        
        self.font = skia.Font(skia.Typeface('Arial'), text_fits - 4)

    # Build, draw the notes
    def build(self, effects):

        # Clear the background
        screen_coords = [0.0, 0.0, self.mmvskia_main.context.width, self.mmvskia_main.context.height]
        bg1 = self.colors["background_1"]
        bg2 = self.colors.get("background_2", bg1)

        # Make the skia Paint and
        screen_paint = skia.Paint(
            AntiAlias = True,
            Style = skia.Paint.kFill_Style,
            Shader = skia.GradientShader.MakeLinear(
                points = [(0.0, 0.0), (screen_coords[2], screen_coords[3])],
                colors = [bg1, bg2]
            ),
        )
        self.mmvskia_main.skia.canvas.drawRect(skia.Rect(*screen_coords), screen_paint)

        # Draw the orientation markers 
        if self.config["do_draw_markers"]:
            self.draw_markers()

        # # Get "needed" variables

        time_first_note = self.vectorial.midi.time_first_note

        # If user passed seconds offset then don't use automatic one from midi file
        if "seconds_offset" in self.config.keys():
            offset = self.config["seconds_offset"]
        else:
            offset = 0
            # offset = time_first_note # .. if audio is trimmed?

        # Offsetted current time at the piano key top most part
        current_time = self.mmvskia_main.context.current_time - offset

        # What keys we'll bother rendering? Check against the first note time offset
        # That's because current_time should be the real midi key not the offsetted one
        accept_minimum_time = current_time - self.config["seconds_of_midi_content"]
        accept_maximum_time = current_time + self.config["seconds_of_midi_content"]

        # What notes are playing? So we draw a darker piano key
        self.notes_playing = {}

        # For each channel of notes
        for channel in self.vectorial.midi.timestamps.keys():

            # For each key message on the timestamps of channels (notes)
            for key in self.vectorial.midi.timestamps[channel]:

                # A "key" is a note if it's an integer
                if isinstance(key, int):

                    # This note play / stop times, for all notes [[start, end], [start, end] ...]
                    note = key
                    times = self.vectorial.midi.timestamps[channel][note]["time"]
                    delete = []

                    # For each note index and the respective interval
                    for index, interval in enumerate(times):

                        # Out of bounds completely, we don't care about this note anymore.
                        # We mark for deletion otherwise it'll mess up the indexing
                        if interval[1] < accept_minimum_time:
                            delete.append(index)
                            continue
                        
                        # Notes past this one are too far from being played and out of bounds
                        if interval[0] > accept_maximum_time:
                            break

                        # Is the current time inside the note? If yes, the note is playing
                        current_time_in_interval = (interval[0] < current_time < interval[1])
                        
                        # Append to playing notes
                        if current_time_in_interval:
                            self.notes_playing[note] = channel

                        # Either way, draw the key
                        self.draw_note(
                            # TODO: No support for velocity yet :(
                            velocity = 128,

                            # Vertical position (start / end)
                            start = interval[0],
                            end = interval[1],

                            # Channel for the color and note for the horizontal position
                            channel = channel,
                            note = note,

                            # Name so we can decide to draw a sharp or plain key
                            name = self.vectorial.midi.note_to_name(note),
                        )
                
                    # This is an interval we do not care about anymore
                    # as the end of the note is past the minimum time we accept a note being rendered
                    for index in reversed(delete):
                        del self.vectorial.midi.timestamps[channel][note]["time"][index]

        self.draw_piano()
예제 #14
0
class MMVSkiaProgressionBarRectangle:
    def __init__(self, MMVSkiaVectorial, mmv):
        self.mmv = mmv
        self.vectorial = MMVSkiaVectorial
        self.config = self.vectorial.config
        self.polar = PolarCoordinates()
        self.functions = Functions()

        # Leave this much of blank space at the left and right edge
        bleed_x = self.mmv.context.width * (1 / 19)

        # Where Y starts and ends, ie. what horizontal line to draw the progression bar on
        if self.config["position"] == "bottom":
            start_end_y = (19 / 20) * self.mmv.context.height

        elif self.config["position"] == "top":
            start_end_y = (1 / 20) * self.mmv.context.height

        # End X we have to count the bleed
        self.start_x = bleed_x
        self.end_x = self.mmv.context.width - bleed_x

        self.start_y = start_end_y
        self.end_y = start_end_y

    # Build, draw the bar
    def build(self, config, effects):

        # Get "needed" variables
        total_steps = self.mmv.context.total_steps
        completion = self.functions.proportion(
            total_steps, 1,
            self.mmv.core.this_step)  # Completion from 0-1 means
        resolution_ratio_multiplier = self.mmv.context.resolution_ratio_multiplier

        # We push the bar downwards according to the avg amplitude for a nice shaky-blur effect
        offset_by_amplitude = self.mmv.core.modulators[
            "average_value"] * self.config[
                "shake_scalar"] * resolution_ratio_multiplier

        if self.config["position"] == "top":
            offset_by_amplitude *= (-1)

        # White full opacity color
        colors = [1, 1, 1, 1]

        # Make a skia color with the colors list as argument
        color = skia.Color4f(*colors)

        # Make the skia Paint and
        paint = skia.Paint(
            AntiAlias=True,
            Color=color,
            Style=skia.Paint.kStroke_Style,
            StrokeWidth=10 * resolution_ratio_multiplier,  # + (magnitude/4),
            # ImageFilter=skia.ImageFilters.DropShadow(3, 3, 5, 5, skia.ColorWHITE),
        )

        # The direction we're walking centered at origin, $\vec{AB} = A - B$
        path_vector = np.array(
            [self.end_x - self.start_x, self.end_y - self.start_y])

        # Proportion we already walked
        path_vector = path_vector * completion

        # Draw the main line starting at the start coordinates, push down by offset_by_amplitude
        path = skia.Path()
        path.moveTo(self.start_x, self.start_y + offset_by_amplitude)
        path.lineTo(self.start_x + path_vector[0],
                    self.start_y + path_vector[1] + offset_by_amplitude)
        self.mmv.skia.canvas.drawPath(path, paint)

        # Borders around image
        if True:
            # Distance away from s
            distance = 9 * resolution_ratio_multiplier

            colors = [1, 1, 1, 0.7]

            # Make a skia color with the colors list as argument
            color = skia.Color4f(*colors)

            # Make the skia Paint and
            paint = skia.Paint(
                AntiAlias=True,
                Color=color,
                Style=skia.Paint.kStroke_Style,
                StrokeWidth=2,
            )

            # Rectangle border
            border = skia.Rect(self.start_x - distance,
                               self.start_y - distance + offset_by_amplitude,
                               self.end_x + distance,
                               self.end_y + distance + offset_by_amplitude)

            # Draw the border
            self.mmv.skia.canvas.drawRect(border, paint)
 def __init__(self):
     self.functions = Functions()
예제 #16
0
class AudioProcessing:
    def __init__(self, depth = LOG_NO_DEPTH) -> None:
        debug_prefix = "[AudioProcessing.__init__]"
        ndepth = depth + LOG_NEXT_DEPTH

        self.fourier = Fourier()
        self.datautils = DataUtils()
        self.functions = Functions()
        self.config = None

        # MMV specific, where we return repeated frequencies from the
        # function process
        self.where_decay_less_than_one = 440
        self.value_at_zero = 5

        # List of full frequencies of notes
        # - 50 to 68 yields freqs of 24.4 Hz up to 
        self.piano_keys_frequencies = [round(self.get_frequency_of_key(x), 2) for x in range(-50, 68)]
        logging.info(f"{depth}{debug_prefix} Whole notes frequencies we'll care: [{self.piano_keys_frequencies}]")

    # Slice a mono and stereo audio data
    def slice_audio(self,
            stereo_data: np.ndarray,
            mono_data: np.ndarray,
            sample_rate: int,
            start_cut: int,
            end_cut: int,
            batch_size: int=None
        ) -> None:
        
        # Cut the left and right points range
        left_slice = stereo_data[0][start_cut:end_cut]
        right_slice = stereo_data[1][start_cut:end_cut]

        # Cut the mono points range
        # mono_slice = mono_data[start_cut:end_cut]

        if not batch_size == None:
            # Empty audio slice array if we're at the end of the audio
            self.audio_slice = np.zeros([3, batch_size])

            # Get the audio slices of the left and right channel
            self.audio_slice[0][ 0:left_slice.shape[0] ] = left_slice
            self.audio_slice[1][ 0:right_slice.shape[0] ] = right_slice
            # self.audio_slice[2][ 0:mono_slice.shape[0] ] = mono_slice

        else:
            # self.audio_slice = [left_slice, right_slice, mono_slice]
            self.audio_slice = [left_slice, right_slice]

        # Calculate average amplitude
        self.average_value = float(np.mean(np.abs(
            mono_data[start_cut:end_cut]
        )))

    def resample(self,
            data: np.ndarray,
            original_sample_rate: int,
            new_sample_rate: int
        ) -> None:

        ratio = new_sample_rate / original_sample_rate
        if ratio == 1:
            return data
        else:
            return samplerate.resample(data, ratio, 'sinc_best')

    # Get N semitones above / below A4 key, 440 Hz
    #
    # get_frequency_of_key(-12) = 220 Hz
    # get_frequency_of_key(  0) = 440 Hz
    # get_frequency_of_key( 12) = 880 Hz
    #
    def get_frequency_of_key(self, n):
        return 440 * (2**(n/12))

    # https://stackoverflow.com/a/2566508
    def find_nearest(self, array, value):
        index = (np.abs(array - value)).argmin()
        return index, array[index]
    
    # Calculate the FFT of this data, get only wanted frequencies based on the musical notes
    def process(self,
            data: np.ndarray,
            original_sample_rate: int,
        ) -> None:
        
        # The returned dictionary
        processed = {}

        # Iterate on config
        for _, value in self.config.items():

            # Get info on config
            sample_rate = value.get("sample_rate")
            start_freq = value.get("start_freq")
            end_freq = value.get("end_freq")

            # Get the frequencies we want and will return in the end
            wanted_freqs = self.datautils.list_items_in_between(
                self.piano_keys_frequencies,
                start_freq, end_freq,
            )

            # Calculate the binned FFT, we get N vectors of [freq, value]
            # of this FFT
            binned_fft = self.fourier.binned_fft(
                # Resample our data to the one specified on the config
                data = self.resample(
                    data = data,
                    original_sample_rate = original_sample_rate,
                    new_sample_rate = sample_rate,
                ),
                # # # # # # # # # # # # # # # # # # # # # # # # # # # #
                sample_rate =  sample_rate,
                original_sample_rate = original_sample_rate,
            )

            # Get the nearest freq and add to processed            
            for freq in wanted_freqs:

                # Get the nearest and FFT value
                nearest = self.find_nearest(binned_fft[0], freq)
                value = binned_fft[1][nearest[0]]
     
                # How much bars we'll render duped at this freq, see
                # this function on the Functions class for more detail
                N = math.ceil(
                    self.functions.how_much_bars_on_this_frequency(
                        x = freq,
                        where_decay_less_than_one = self.where_decay_less_than_one,
                        value_at_zero = self.value_at_zero,
                    )
                )

                # Add repeated bars or just one
                for i in range(N):
                    processed[nearest[1] + (i/10)] = value
                
        linear_processed_fft = []
        frequencies = []

        for frequency, value in processed.items():
            frequencies.append(frequency)
            linear_processed_fft.append(value)
        
        return [linear_processed_fft, frequencies]
jumpcutter = interface.get_jumpcutter()

# Configure with stuff
jumpcutter.configure(
    batch_size=AUDIO_BATCH_SIZE,
    sample_rate=48000,
    target_fps=ORIGINAL_FRAMERATE,
)

# Read source audio file
jumpcutter.init(audio_path=INPUT_AUDIO_FILE)

mode = args.kflags.get("mode", "render")

# Stuff we'll need
functions = Functions()

if mode == "view":
    shader_interface = interface.get_shader_interface()
    mgl = shader_interface.get_mgl_interface(master_shader=True, flip=True)

    MSAA = 8

    # # MGL bootstrap

    # Set up target render configuration
    mgl.target_render_settings(
        width=WIDTH,
        height=HEIGHT,
        ssaa=1,
        fps=ORIGINAL_FRAMERATE,
class MMVMusicBarsCircle:
    def __init__(self, MMVVectorial, context, skia_object):
        self.vectorial = MMVVectorial
        self.context = context
        self.skia = skia_object
        self.config = self.vectorial.config
        self.polar = PolarCoordinates()
        self.functions = Functions()

        self.center_x = self.vectorial.context.width / 2
        self.center_y = self.vectorial.context.height / 2

    def build(self, fitted_ffts: dict, frequencies: list, this_step: int,
              config: dict, effects):

        resolution_ratio_multiplier = self.vectorial.context.resolution_ratio_multiplier

        if self.config["mode"] == "symetric":

            data = {}

            for channel in (["l", "r"]):

                data[channel] = {
                    "coordinates": [],
                    "paints": [],
                }

                this_channel_fft = fitted_ffts[channel]
                npts = len(this_channel_fft)

                for index, magnitude in enumerate(this_channel_fft):

                    if channel == "l":
                        theta = (math.pi / 2) - ((index / npts) * math.pi)

                    elif channel == "r":
                        theta = (math.pi / 2) + ((index / npts) * math.pi)

                    magnitude = (magnitude / 720) * self.context.height

                    minimum_multiplier = 0.4
                    maximum_multiplier = 17

                    size = (magnitude) * self.functions.ax_plus_b_two_points(
                        x=frequencies[0][index],
                        end_x=20000,
                        zero_value=minimum_multiplier,
                        max_value=maximum_multiplier,
                    )

                    # size = (magnitude*6) * ( (( (maximum_multiplier - minimum_multiplier)*index) / len(this_channel_fft)) + minimum_multiplier )

                    # We send an r, theta just in case we want to do something with it later on
                    data[channel]["coordinates"].append([
                        # ( self.config["minimum_bar_size"] + magnitude*(2e6) ) * effects["size"],
                        (self.config["minimum_bar_size"] + size) *
                        effects["size"],
                        theta,
                    ])

                    # Rotate the colors a bit on each step
                    theta += this_step / 100

                    # Define the color of the bars
                    colors = [
                        abs(math.sin((theta / 2))),
                        abs(math.sin((theta + ((1 / 3) * 2 * math.pi)) / 2)),
                        abs(math.sin((theta + ((2 / 3) * 2 * math.pi)) / 2)),
                    ] + [0.7]  # Add full opacity

                    # Make a skia color with the colors list as argument
                    color = skia.Color4f(*colors)

                    # Make the skia Paint and
                    paint = skia.Paint(
                        AntiAlias=True,
                        Color=color,
                        Style=skia.Paint.kStroke_Style,
                        StrokeWidth=8 *
                        resolution_ratio_multiplier  # + (magnitude/4),
                    )

                    # Store it on a list do draw in the end
                    data[channel]["paints"].append(paint)

            # Our list of coordinates and paints, invert the right channel for drawing the path in the right direction
            # Not reversing it will yield "symetric" bars along the diagonal
            coordinates = data["l"]["coordinates"] + [
                x for x in reversed(data["r"]["coordinates"])
            ]
            paints = data["l"]["paints"] + [
                x for x in reversed(data["r"]["paints"])
            ]

            # Filled background
            if False:  # self.config["draw_background"]

                path = skia.Path()
                white_background = skia.Paint(
                    AntiAlias=True,
                    Color=skia.ColorWHITE,
                    Style=skia.Paint.kFill_Style,
                    StrokeWidth=3,
                    ImageFilter=skia.ImageFilters.DropShadow(
                        3, 3, 5, 5, skia.ColorBLACK),
                    MaskFilter=skia.MaskFilter.MakeBlur(
                        skia.kNormal_BlurStyle, 1.0))

                more = 1.05

                self.polar.from_r_theta(coordinates[0][0] * more,
                                        coordinates[0][1])
                polar_offset = self.polar.get_rectangular_coordinates()

                path.moveTo(
                    (self.center_x + polar_offset[0]),
                    (self.center_y + polar_offset[1]),
                )

                for coord_index, coord in enumerate(coordinates):

                    # TODO: implement this function in DataUtils for not repeating myself
                    get_nearby = 4

                    size_coordinates = len(coordinates)
                    real_state = coordinates * 3

                    nearby_coordinates = real_state[size_coordinates + (
                        coord_index - get_nearby):size_coordinates +
                                                    (coord_index + get_nearby)]

                    # [0, 1, 2, 3, 4] --> weights=
                    #  3  4  5, 4, 3

                    n = len(nearby_coordinates)

                    weights = [n - abs((n / 2) - x) for x in range(n)]

                    s = 0
                    for index, item in enumerate(nearby_coordinates):
                        s += item[0] * weights[index]

                    avg_coord = s / sum(weights)

                    self.polar.from_r_theta(avg_coord * more, coord[1])

                    polar_offset = self.polar.get_rectangular_coordinates()

                    path.lineTo(
                        (self.center_x + polar_offset[0]),
                        (self.center_y + polar_offset[1]),
                    )

                self.skia.canvas.drawPath(path, white_background)

            # Countour, stroke
            if False:  # self.config["draw_black_border"]

                more = 2

                path = skia.Path()

                black_stroke = skia.Paint(
                    AntiAlias=True,
                    Color=skia.ColorWHITE,
                    Style=skia.Paint.kStroke_Style,
                    StrokeWidth=6,
                    ImageFilter=skia.ImageFilters.DropShadow(
                        3, 3, 5, 5, skia.ColorWHITE),
                    MaskFilter=skia.MaskFilter.MakeBlur(
                        skia.kNormal_BlurStyle, 1.0))

                for coord_index, coord in enumerate(coordinates):

                    get_nearby = 10

                    size_coordinates = len(coordinates)
                    real_state = coordinates * 3

                    nearby_coordinates = real_state[size_coordinates + (
                        coord_index - get_nearby):size_coordinates +
                                                    (coord_index + get_nearby)]

                    n = len(nearby_coordinates)

                    weights = [n - abs((n / 2) - x) for x in range(n)]

                    s = 0
                    for index, item in enumerate(nearby_coordinates):
                        s += item[0] * weights[index]

                    avg_coord = s / sum(weights)

                    self.polar.from_r_theta(
                        self.config["minimum_bar_size"] +
                        ((avg_coord - self.config["minimum_bar_size"]) * more),
                        coord[1])
                    polar_offset = self.polar.get_rectangular_coordinates()

                    coords = [(self.center_x + polar_offset[0]),
                              (self.center_y + polar_offset[1])]

                    if coord_index == 0:
                        path.moveTo(*coords)
                    path.lineTo(*coords)

                self.skia.canvas.drawPath(path, black_stroke)

            # Draw the main bars
            for index, coord in enumerate(coordinates):

                more = 1

                path = skia.Path()
                path.moveTo(self.center_x, self.center_y)

                self.polar.from_r_theta(coord[0], coord[1])
                polar_offset = self.polar.get_rectangular_coordinates()

                path.lineTo(
                    (self.center_x + polar_offset[0]) * more,
                    (self.center_y + polar_offset[1]) * more,
                )

                self.skia.canvas.drawPath(path, paints[index])
class MMVPianoRollTopDown:
    def __init__(self, MMVVectorial, context, skia_object, midi):
        self.vectorial = MMVVectorial
        self.context = context
        self.skia = skia_object
        self.midi = midi
        self.config = self.vectorial.config
        self.functions = Functions()
        self.datautils = DataUtils()
        self.piano_keys = {}
        self.keys_centers = {}

        self.background_color = 22 / 255

    # Bleed is extra keys you put at the lower most and upper most ranged keys
    def generate_piano(self, min_note, max_note, bleed=3):

        # NOTE: EDIT HERE STATIC VARIABLES  TODO: set them on config
        self.piano_height = (3.5 / 19) * self.vectorial.context.height
        self.viewport_height = self.vectorial.context.height - self.piano_height

        # TODO: set these variables in config
        self.seconds_of_midi_content = 3

        for key_index in range(min_note - bleed, max_note + bleed):
            next_key = PianoKey(self.midi, self.skia, self.background_color)
            next_key.by_key_index(key_index)
            self.piano_keys[key_index] = next_key

        print(len(self.piano_keys.keys()))

        # We get the center of keys based on the "distance walked" in between intervals
        # As black keys are in between two white keys, we walk half white key width on those
        # and a full white key between E and F, B and C.

        divisions = 0

        for index, key_index in enumerate(self.piano_keys.keys()):

            key = self.piano_keys[key_index]

            # First index of key can't compare to previous key, starts at current_center
            if not index == 0:

                prevkey = self.piano_keys[key_index - 1]

                # Distance is a tone
                if (prevkey.is_white) and (key.is_white):
                    divisions += 2

                # Distance is a semitone
                else:
                    divisions += 1

        # for key_index in self.piano_keys.keys():
        #     if "#" in self.piano_keys[key_index].name:
        #         divisions += 1
        #     else:
        #         divisions += 2

        # Now we have the divisions, we can calculate the real key width based on the resolution

        # print("width", self.vectorial.context.width)

        # The keys intervals for walking
        self.semitone_width = self.vectorial.context.width / divisions
        self.tone_width = self.semitone_width * 2

        # print(self.semitone_width, self.tone_width, divisions)
        # exit()

        # Current center we're at
        current_center = 0

        for index, key_index in enumerate(self.piano_keys.keys()):

            key = self.piano_keys[key_index]

            # First index of key can't compare to previous key, starts at current_center
            if not index == 0:

                prevkey = self.piano_keys[key_index - 1]

                # Distance is a tone
                if (prevkey.is_white) and (key.is_white):
                    current_center += self.tone_width

                # Distance is a semitone
                else:
                    current_center += self.semitone_width

            # Set the
            if key.is_white:
                this_note_width = self.tone_width
            else:
                this_note_width = (4 / 6) * self.tone_width

            self.piano_keys[key_index].width = this_note_width
            self.piano_keys[key_index].height = self.piano_height
            self.piano_keys[
                key_index].resolution_height = self.vectorial.context.height
            self.piano_keys[key_index].center_x = current_center

            self.keys_centers[key_index] = current_center

    def draw_piano(self):
        for key_index in self.piano_keys.keys():
            if self.piano_keys[key_index].is_white:
                self.piano_keys[key_index].draw(self.notes_playing)

        for key_index in self.piano_keys.keys():
            if self.piano_keys[key_index].is_black:
                self.piano_keys[key_index].draw(self.notes_playing)

    def draw_markers(self):

        c = 0.35
        white_white_color = skia.Color4f(c, c, c, 1)

        white_white_paint = skia.Paint(
            AntiAlias=True,
            Color=white_white_color,
            Style=skia.Paint.kStroke_Style,
            StrokeWidth=1,
        )

        current_center = 0

        for index, key_index in enumerate(self.piano_keys.keys()):
            key = self.piano_keys[key_index]
            if not index == 0:
                prevkey = self.piano_keys[key_index - 1]
                if (prevkey.is_white) and (key.is_white):
                    current_center += self.tone_width
                    rect = skia.Rect(current_center - self.semitone_width, 0,
                                     current_center + 1 - self.semitone_width,
                                     self.vectorial.context.height)
                    self.skia.canvas.drawRect(rect, white_white_paint)
                else:
                    current_center += self.semitone_width

        for key_index in self.piano_keys.keys():
            self.piano_keys[key_index].draw_marker()

    def draw_note(self, velocity, start, end, channel, note, name):

        color_channels = {
            0: {
                "plain": ImageColor.getcolor("#ffcc00", "RGB"),
                "sharp": ImageColor.getcolor("#ff9d00", "RGB"),
            },
            1: {
                "plain": ImageColor.getcolor("#00ff0d", "RGB"),
                "sharp": ImageColor.getcolor("#00a608", "RGB"),
            },
            2: {
                "plain": ImageColor.getcolor("#6600ff", "RGB"),
                "sharp": ImageColor.getcolor("#39008f", "RGB"),
            },
            3: {
                "plain": ImageColor.getcolor("#ff0000", "RGB"),
                "sharp": ImageColor.getcolor("#990000", "RGB"),
            },
            4: {
                "plain": ImageColor.getcolor("#00fffb", "RGB"),
                "sharp": ImageColor.getcolor("#00b5b2", "RGB"),
            },
            5: {
                "plain": ImageColor.getcolor("#ff006f", "RGB"),
                "sharp": ImageColor.getcolor("#a10046", "RGB"),
            },
            6: {
                "plain": ImageColor.getcolor("#aaff00", "RGB"),
                "sharp": ImageColor.getcolor("#75b000", "RGB"),
            },
            7: {
                "plain": ImageColor.getcolor("#e1ff00", "RGB"),
                "sharp": ImageColor.getcolor("#a9bf00", "RGB"),
            },
            8: {
                "plain": ImageColor.getcolor("#ff3300", "RGB"),
                "sharp": ImageColor.getcolor("#a82200", "RGB"),
            },
            9: {
                "plain": ImageColor.getcolor("#00ff91", "RGB"),
                "sharp": ImageColor.getcolor("#00b567", "RGB"),
            },
            10: {
                "plain": ImageColor.getcolor("#ff00aa", "RGB"),
                "sharp": ImageColor.getcolor("#c40083", "RGB"),
            },
            11: {
                "plain": ImageColor.getcolor("#c800ff", "RGB"),
                "sharp": ImageColor.getcolor("#8e00b5", "RGB"),
            },
            12: {
                "plain": ImageColor.getcolor("#00ff4c", "RGB"),
                "sharp": ImageColor.getcolor("#00c93c", "RGB"),
            },
            13: {
                "plain": ImageColor.getcolor("#ff8a8a", "RGB"),
                "sharp": ImageColor.getcolor("#bf6767", "RGB"),
            },
            14: {
                "plain": ImageColor.getcolor("#ffde7d", "RGB"),
                "sharp": ImageColor.getcolor("#c4aa5e", "RGB"),
            },
            15: {
                "plain": ImageColor.getcolor("#85ebff", "RGB"),
                "sharp": ImageColor.getcolor("#5ca7b5", "RGB"),
            },
            16: {
                "plain": ImageColor.getcolor("#ff7aa4", "RGB"),
                "sharp": ImageColor.getcolor("#bd5978", "RGB"),
            },
            "default": {
                "plain": ImageColor.getcolor("#dddddd", "RGB"),
                "sharp": ImageColor.getcolor("#ffffff", "RGB"),
            },
        }

        note_colors = color_channels.get(channel, color_channels["default"])

        if "#" in name:
            width = self.semitone_width * 0.9
            c = note_colors["sharp"]
            color = skia.Color4f(c[0] / 255, c[1] / 255, c[2] / 255, 1)

        else:
            width = self.tone_width * 0.6
            c = note_colors["plain"]
            color = skia.Color4f(c[0] / 255, c[1] / 255, c[2] / 255, 1)

        # Make the skia Paint and
        paint = skia.Paint(
            AntiAlias=True,
            Color=color,
            Style=skia.Paint.kFill_Style,
            # Shader=skia.GradientShader.MakeLinear(
            #     points=[(0.0, 0.0), (self.vectorial.context.width, self.vectorial.context.height)],
            #     colors=[skia.Color4f(0, 0, 1, 1), skia.Color4f(0, 1, 0, 1)]),
            StrokeWidth=2,
        )

        # c = ImageColor.getcolor("#d1ce1d", "RGB")
        c = (0, 0, 0)
        border = skia.Paint(
            AntiAlias=True,
            Color=skia.Color4f(c[0] / 255, c[1] / 255, c[2] / 255, 1),
            Style=skia.Paint.kStroke_Style,
            # ImageFilter=skia.ImageFilters.DropShadow(3, 3, 5, 5, skia.ColorBLUE),
            # MaskFilter=skia.MaskFilter.MakeBlur(skia.kNormal_BlurStyle, 2.0),
            StrokeWidth=max(
                self.vectorial.context.resolution_ratio_multiplier * 2, 1),
        )

        x = self.keys_centers[note]

        y = self.functions.proportion(
            self.seconds_of_midi_content,
            self.viewport_height,  #*2,
            self.vectorial.context.current_time - start)

        height = self.functions.proportion(self.seconds_of_midi_content,
                                           self.viewport_height, end - start)

        coords = [
            x - (width / 2),
            y + (self.viewport_height) - height,
            x + (width / 2),
            y + (self.viewport_height),
        ]

        # Rectangle border
        rect = skia.Rect(*coords)

        # Draw the border
        self.skia.canvas.drawRect(rect, paint)
        self.skia.canvas.drawRect(rect, border)

    # Build, draw the bar
    def build(self, fftinfo, this_step, config, effects):

        c = self.background_color
        self.skia.canvas.clear(skia.Color4f(c, c, c, 1))
        self.seconds_of_midi_content = config["seconds-of-midi-content"]
        SECS_OFFSET = config["seconds-offset"]

        self.draw_markers()

        # Get "needed" variables
        current_time = self.vectorial.context.current_time + SECS_OFFSET
        resolution_ratio_multiplier = self.vectorial.context.resolution_ratio_multiplier

        # contents = self.datautils.dictionary_items_in_between(
        #     self.midi.timestamps,
        #     current_time - 2,
        #     current_time + self.seconds_of_midi_content * 2
        # )

        accept_minimum_time = current_time - self.seconds_of_midi_content
        accept_maximum_time = current_time + self.seconds_of_midi_content

        self.notes_playing = []

        for channel in self.midi.timestamps.keys():
            for key in self.midi.timestamps[channel]:
                if isinstance(key, int):

                    note = key
                    times = self.midi.timestamps[channel][note]["time"]
                    delete = []

                    for index, interval in enumerate(times):

                        # Out of bounds
                        if interval[1] < accept_minimum_time:
                            delete.append(index)
                            continue

                        if interval[0] > accept_maximum_time:
                            break

                        current_time_in_interval = (interval[0] < current_time
                                                    < interval[1])
                        accepted_render = (accept_minimum_time < current_time <
                                           accept_maximum_time)

                        if current_time_in_interval:
                            self.notes_playing.append(note)

                        if current_time_in_interval or accepted_render:
                            self.draw_note(
                                velocity=128,
                                start=interval[0] - SECS_OFFSET,
                                end=interval[1] - SECS_OFFSET,
                                channel=channel,
                                note=note,
                                name=self.midi.note_to_name(note),
                            )

                    for index in reversed(delete):
                        del self.midi.timestamps[channel][note]["time"][index]

        self.draw_piano()
예제 #20
0
class Interpolation:
    def __init__(self):
        self.functions = Functions()

    """
        Linear, between point A and B based on a current "step" and total steps
    kwargs: {
        "start_value": float, target start value
        "target_value": float, target end value
        "current_step": float, this step out of the total
        "total_steps": float, target total steps to finish the interpolation
    }
    """

    def linear(self, **kwargs) -> float:

        # Out of bounds in steps, return target value
        if kwargs["current_step"] > kwargs["total_steps"]:
            return kwargs["target_value"]

        # How much a walked part is? difference between target and start, divided by total steps
        part = (kwargs["target_value"] -
                kwargs["start_value"]) / kwargs["total_steps"]

        # How much we walk from start to finish in that proportion
        walked = part * kwargs["current_step"]

        # Return the proportion walked plus the start value
        return kwargs["start_value"] + walked

    """
        "Biased" remaining linear, walks remaining distance times the ratio
        ratio, 0.05 is smooth, 0.1 is medium, 1 is instant
        random is a decimal that adds to ratio randomly
    
    kwargs: {
        "start_value": float, target start value (returned if current_step is zero)
        "target_value": float, target value to reach
        "current_step": float, only used if 0 to signal start of interpolation
        "current_value": float, the position we're at to calculate the remaining distance
        "ratio": float, walk remaining distance times this ratio from current value
        "ratio_randomness": float=0, add a random value from 0 to this to the ratio
    }
    """

    def remaining_approach(self, **kwargs) -> float:

        # We're at the first step, so start on current value
        if kwargs["current_step"] == 0:
            return kwargs["start_value"]

        # Remaining distance is difference between target and current, ratio is the ratio plus a random or 0 value
        remaining_distance = (kwargs["target_value"] - kwargs["current_value"])
        ratio = kwargs["ratio"] + random.uniform(
            0, kwargs.get("ratio_randomness", 0))

        return kwargs["current_value"] + (remaining_distance * ratio)

    # Sigmoid activation between two points, smoothed out "linear" curver
    def sigmoid(
        self,
        start_value: float,
        target_value: float,
        smooth: float,
    ) -> float:

        distance = (target_value - start_value)
        where = self.functions.proportion(total, 1, current)
        walk = distance * self.functions.sigmoid(where, smooth)
        return a + walk
예제 #21
0
class MMVSkiaPianoRollTopDown:
    def __init__(self, mmv, MMVSkiaPianoRollVectorial):
        self.mmvskia_main = mmv
        self.vectorial = MMVSkiaPianoRollVectorial
        self.config = self.vectorial.config
        self.functions = Functions()
        self.datautils = DataUtils()
        self.piano_keys = {}
        self.keys_centers = {}
        """
        When converting the colors we divide by 255 so we normalize in a value in between 0 and 1
        Because skia works like that.
        """

        sep = os.path.sep

        # Load the yaml config file
        color_preset = self.config["color_preset"]
        color_config_yaml = self.mmvskia_main.utils.load_yaml(
            f"{self.mmvskia_main.mmvskia_interface.top_level_interace.data_dir}{sep}mmvskia{sep}piano_roll{sep}color_{color_preset}.yaml"
        )

        # Get the global colors into a dictionary

        self.global_colors = {}

        for key in color_config_yaml["global"]:
            self.global_colors[key] = [
                channel / 255 for channel in ImageColor.getcolor(
                    "#" + str(color_config_yaml["global"][key]), "RGB")
            ]

        # # Get the note colors based on their channel

        self.color_channels = {}

        # For every channel config
        for key in color_config_yaml["channels"]:

            # Create empty dir
            self.color_channels[key] = {}

            # Get colors of sharp and plain, border, etc..
            for color_type in color_config_yaml["channels"][key].keys():

                # Hexadecimal representation of color
                color_hex = color_config_yaml["channels"][key][color_type]

                # Assign RGB value
                self.color_channels[key][color_type] = [
                    channel / 255
                    for channel in ImageColor.getcolor(f"#{color_hex}", "RGB")
                ]

    # Bleed is extra keys you put at the lower most and upper most ranged keys
    def generate_piano(self, min_note, max_note):

        debug_prefix = "[MMVSkiaPianoRollTopDown.generate_piano]"

        # NOTE: EDIT HERE STATIC VARIABLES  TODO: set them on config
        self.piano_height = (3.5 / 19) * self.mmvskia_main.context.height
        self.viewport_height = self.mmvskia_main.context.height - self.piano_height

        # Pretty print
        print("Add key by index: {", end="", flush=True)

        # For each key index, with a bleed (extra keys) counting up and down from the minimum / maximum key index
        for key_index in range(min_note - self.config["bleed"],
                               max_note + self.config["bleed"]):

            # Create a PianoKey object and set its index
            next_key = PianoKey(self.mmvskia_main, self.vectorial, self)
            next_key.by_key_index(key_index)

            # Pretty print
            if not key_index == max_note + self.config["bleed"] - 1:
                print(", ", end="", flush=True)
            else:
                print("}")

            # Add to the piano keys list
            self.piano_keys[key_index] = next_key

        print(debug_prefix, "There will be", len(self.piano_keys.keys()),
              "keys in the piano roll")

        # We get the center of keys based on the "distance walked" in between intervals
        # As black keys are in between two white keys, we walk half white key width on those
        # and a full white key between E and F, B and C.

        # Get number of semitones (divisions) so we can calculate the semitone width afterwards
        divisions = 0

        # For each index and key index (you'll se in a while why)
        for index, key_index in enumerate(self.piano_keys.keys()):

            # Get this key object
            key = self.piano_keys[key_index]

            # First index of key can't compare to previous key, starts at current_center
            if not index == 0:

                # The previous key can be a white or black key, but there isn't a previous at index=0
                prevkey = self.piano_keys[key_index - 1]

                # Both are True, add two, one is True, add one
                # 2 is a tone distance
                # 1 is a semitone distance
                # [True is 1 in Python]
                divisions += (prevkey.is_white) + (key.is_white)

        # The keys intervals for walking
        self.semitone_width = self.mmvskia_main.context.width / divisions
        self.tone_width = self.semitone_width * 2

        # Current center we're at on the X axis so we send to the keys their positions
        current_center = 0

        # Same loop, ignore index=0
        for index, key_index in enumerate(self.piano_keys.keys()):

            # Get this key object
            key = self.piano_keys[key_index]

            # First index of key can't compare to previous key, starts at current_center
            if not index == 0:

                prevkey = self.piano_keys[key_index - 1]

                # Distance is a tone
                if (prevkey.is_white) and (key.is_white):
                    current_center += self.tone_width

                # Distance is a semitone
                else:
                    current_center += self.semitone_width

            # Set the note length according to if it's white or black
            if key.is_white:
                this_note_width = self.tone_width
            else:
                this_note_width = (4 / 6) * self.tone_width

            # Set attributes to this note we're looping
            self.piano_keys[key_index].width = this_note_width
            self.piano_keys[key_index].height = self.piano_height
            self.piano_keys[
                key_index].resolution_height = self.mmvskia_main.context.height
            self.piano_keys[key_index].center_x = current_center

            # And add to the key centers list this key center_x
            self.keys_centers[key_index] = current_center

    # Draw the piano, first draw the white then the black otherwise we have overlaps
    def draw_piano(self):
        # White keys
        for key_index in self.piano_keys.keys():
            if self.piano_keys[key_index].is_white:
                self.piano_keys[key_index].draw(self.notes_playing)

        # Black keys
        for key_index in self.piano_keys.keys():
            if self.piano_keys[key_index].is_black:
                self.piano_keys[key_index].draw(self.notes_playing)

    # Draw the markers IN BETWEEN TWO WHITE KEYS
    def draw_markers(self):

        # The paint of markers in between white keys
        white_white_paint = skia.Paint(
            AntiAlias=True,
            Color=skia.Color4f(
                *self.global_colors["marker_color_between_two_white"], 1),
            Style=skia.Paint.kStroke_Style,
            StrokeWidth=1,
        )

        current_center = 0

        # Check if prev and this key is white, keeps track of a current_center, create rect to draw and draw
        for index, key_index in enumerate(self.piano_keys.keys()):
            key = self.piano_keys[key_index]
            if not index == 0:
                prevkey = self.piano_keys[key_index - 1]
                if (prevkey.is_white) and (key.is_white):
                    current_center += self.tone_width
                    rect = skia.Rect(current_center - self.semitone_width, 0,
                                     current_center - self.semitone_width,
                                     self.mmvskia_main.context.height)
                    self.mmvskia_main.skia.canvas.drawRect(
                        rect, white_white_paint)
                else:
                    current_center += self.semitone_width

        # Ask each key to draw their CENTERED marker
        for key_index in self.piano_keys.keys():
            self.piano_keys[key_index].draw_marker()

    # Draw a given note according to the seconds of midi content on the screen,
    # horizontal (note), vertical (start / end time in seconds) and color (channel)
    def draw_note(self, velocity, start, end, channel, note, name):

        # Get the note colors for this channel, we receive a dict with "sharp" and "plain" keys
        note_colors = self.color_channels.get(channel,
                                              self.color_channels["default"])

        # Is a sharp key
        if "#" in name:
            width = self.semitone_width * 0.9
            color = skia.Color4f(*note_colors["sharp"], 1)

        # Plain key
        else:
            width = self.tone_width * 0.6
            color = skia.Color4f(*note_colors["plain"], 1)

        # Make the skia Paint
        note_paint = skia.Paint(
            AntiAlias=True,
            Color=color,
            Style=skia.Paint.kFill_Style,
            # Shader=skia.GradientShader.MakeLinear(
            #     points=[(0.0, 0.0), (self.mmvskia_main.context.width, self.mmvskia_main.context.height)],
            #     colors=[skia.Color4f(0, 0, 1, 1), skia.Color4f(0, 1, 0, 1)]),
            StrokeWidth=2,
        )

        # Border of the note
        note_border_paint = skia.Paint(
            AntiAlias=True,
            Color=skia.Color4f(*note_colors["border"], 1),
            Style=skia.Paint.kStroke_Style,
            # ImageFilter=skia.ImageFilters.DropShadow(3, 3, 5, 5, skia.ColorBLUE),
            # MaskFilter=skia.MaskFilter.MakeBlur(skia.kNormal_BlurStyle, 2.0),
            StrokeWidth=max(
                self.mmvskia_main.context.resolution_ratio_multiplier * 2, 1),
        )

        # Horizontal we have it based on the tones and semitones we calculated previously
        # this is the CENTER of the note
        x = self.keys_centers[note]

        # The Y is a proportion of, if full seconds of midi content, it's the viewport height itself,
        # otherwise it's a proportion to the processing time according to a start value in seconds
        y = self.functions.proportion(
            self.config["seconds_of_midi_content"],
            self.viewport_height,  #*2,
            self.mmvskia_main.context.current_time - start)

        # The height is just the proportion of, seconds of midi content is the maximum height
        # how much our key length (end - start) is according to that?
        height = self.functions.proportion(
            self.config["seconds_of_midi_content"], self.viewport_height,
            end - start)

        # Build the coordinates of the note
        # Note: We add and subtract half a width because X is the center
        # while we need to add from the viewport out heights on the Y
        coords = [
            x - (width / 2),
            y + (self.viewport_height) - height,
            x + (width / 2),
            y + (self.viewport_height),
        ]

        # Rectangle border of the note
        rect = skia.Rect(*coords)

        # Draw the note and border
        self.mmvskia_main.skia.canvas.drawRect(rect, note_paint)
        self.mmvskia_main.skia.canvas.drawRect(rect, note_border_paint)

    # Build, draw the notes
    def build(self, effects):

        # Clear the background
        self.mmvskia_main.skia.canvas.clear(
            skia.Color4f(*self.global_colors["background"], 1))

        # Draw the orientation markers
        if self.config["do_draw_markers"]:
            self.draw_markers()

        # # Get "needed" variables

        time_first_note = self.vectorial.midi.time_first_note

        # If user passed seconds offset then don't use automatic one from midi file
        if "seconds_offset" in self.config.keys():
            offset = self.config["seconds_offset"]
        else:
            offset = 0
            # offset = time_first_note # .. if audio is trimmed?

        # Offsetted current time at the piano key top most part
        current_time = self.mmvskia_main.context.current_time - offset

        # What keys we'll bother rendering? Check against the first note time offset
        # That's because current_time should be the real midi key not the offsetted one
        accept_minimum_time = current_time - self.config[
            "seconds_of_midi_content"]
        accept_maximum_time = current_time + self.config[
            "seconds_of_midi_content"]

        # What notes are playing? So we draw a darker piano key
        self.notes_playing = []

        # For each channel of notes
        for channel in self.vectorial.midi.timestamps.keys():

            # For each key message on the timestamps of channels (notes)
            for key in self.vectorial.midi.timestamps[channel]:

                # A "key" is a note if it's an integer
                if isinstance(key, int):

                    # This note play / stop times, for all notes [[start, end], [start, end] ...]
                    note = key
                    times = self.vectorial.midi.timestamps[channel][note][
                        "time"]
                    delete = []

                    # For each note index and the respective interval
                    for index, interval in enumerate(times):

                        # Out of bounds completely, we don't care about this note anymore.
                        # We mark for deletion otherwise it'll mess up the indexing
                        if interval[1] < accept_minimum_time:
                            delete.append(index)
                            continue

                        # Notes past this one are too far from being played and out of bounds
                        if interval[0] > accept_maximum_time:
                            break

                        # Is the current time inside the note? If yes, the note is playing
                        current_time_in_interval = (interval[0] < current_time
                                                    < interval[1])

                        # Append to playing notes
                        if current_time_in_interval:
                            self.notes_playing.append(note)

                        # Either way, draw the key
                        self.draw_note(
                            # TODO: No support for velocity yet :(
                            velocity=128,

                            # Vertical position (start / end)
                            start=interval[0],
                            end=interval[1],

                            # Channel for the color and note for the horizontal position
                            channel=channel,
                            note=note,

                            # Name so we can decide to draw a sharp or plain key
                            name=self.vectorial.midi.note_to_name(note),
                        )

                    # This is an interval we do not care about anymore
                    # as the end of the note is past the minimum time we accept a note being rendered
                    for index in reversed(delete):
                        del self.vectorial.midi.timestamps[channel][note][
                            "time"][index]

        self.draw_piano()
예제 #22
0
class AudioProcessing:
    def __init__(self) -> None:
        debug_prefix = "[AudioProcessing.__init__]"

        # Create some util classes
        self.fourier = Fourier()
        self.datautils = DataUtils()
        self.functions = Functions()
        self.config = None

        # List of full frequencies of notes
        # - 50 to 68 yields freqs of 24.4 Hz up to
        self.piano_keys_frequencies = [
            round(self.get_frequency_of_key(x), 2) for x in range(-50, 68)
        ]

    # Get specs on config dictionary
    def _get_config_stuff(self, config_dict):

        # Get config
        start_freq = config_dict["start_freq"]
        end_freq = config_dict["end_freq"]

        # Get the frequencies we want and will return in the end
        wanted_freqs = self.datautils.list_items_in_between(
            self.piano_keys_frequencies,
            start_freq,
            end_freq,
        )

        # Counter for expected frequencies on this config
        expected_N_frequencies = 0
        expected_frequencies = []

        # Add target freq if it's not on the list
        for freq in wanted_freqs:

            # How much bars we'll render duped at this freq, see
            # this function on the Functions class for more detail
            N = math.ceil(
                self.functions.how_much_bars_on_this_frequency(
                    x=freq,
                    where_decay_less_than_one=self.where_decay_less_than_one,
                    value_at_zero=self.value_at_zero,
                ))

            # Add to total freqs the amount we expect
            expected_N_frequencies += N

            # Add individual frequencies
            expected_frequencies.extend([freq + (i / 100) for i in range(N)])

        # Return info
        return {
            "original_sample_rate": config_dict["original_sample_rate"],
            "target_sample_rate": config_dict["target_sample_rate"],
            "expected_N_frequencies": expected_N_frequencies,
            "expected_frequencies": expected_frequencies,
            "start_freq": start_freq,
            "end_freq": end_freq,
        }

    # Set up a configuration list of dicts, They can look like this:
    """
    [ {
        "original_sample_rate": 48000,
        "target_sample_rate": 5000,
        "start_freq": 20,
        "end_freq": 2500,
    }, {
        ...
    }]
    """

    # NOTE: The FFT will only get values of frequencies up to SAMPLE_RATE/2 and jumps of
    # the calculation sample rate divided by the window size (batch size)
    # So if you want more bass information, downsample to 5000 Hz or 1000 Hz and get frequencies
    # up to 2500 or 500, respectively.
    def configure(self,
                  config,
                  where_decay_less_than_one=440,
                  value_at_zero=3):
        debug_prefix = "[AudioProcessing.configure]"

        logging.info(
            f"{debug_prefix} Whole notes frequencies we'll care: [{self.piano_keys_frequencies}]"
        )

        # Assign
        self.config = config
        self.FFT_length = 0
        self.where_decay_less_than_one = where_decay_less_than_one
        self.value_at_zero = value_at_zero

        # The configurations on sample rate, frequencies to expect
        self.process_layer_configs = []

        # For every config dict on the config
        for layers in self.config:
            info = self._get_config_stuff(layers)
            self.FFT_length += info["expected_N_frequencies"] * 2
            self.process_layer_configs.append(info)

        # The size will be half, because left and right channel so we multiply by 2
        # self.FFT_length *= 2
        print("BINNED FFT LENGTH", self.FFT_length)

    # # Feature Extraction

    # Calculate the Root Mean Square
    def rms(self, values: np.ndarray) -> float:
        return np.sqrt(np.mean(values**2))

    # # New Methods

    # Yield information on the audio slice
    def get_info_on_audio_slice(self,
                                audio_slice: np.ndarray,
                                original_sample_rate,
                                do_calculate_fft=True) -> dict:
        N = audio_slice.shape[1]

        # Calculate MONO
        mono = (audio_slice[0] + audio_slice[1]) / 2

        yield ["mmv_raw_audio_left", audio_slice[0]]
        yield ["mmv_raw_audio_right", audio_slice[1]]

        # # Average audio amplitude based on RMS

        # L, R, Mono respectively
        RMS = []

        # Iterate, calculate the median of the absolute values
        for channel_number in [0, 1]:
            RMS.append(np.sqrt(np.mean(audio_slice[channel_number][0:N]**2)))
            # RMS.append(np.median(np.abs(audio_slice[channel_number][0:N//120])))

        # Append mono average amplitude
        RMS.append(sum(RMS) / 2)

        # Yield average amplitudes info
        yield ["mmv_rms", tuple([round(value, 8) for value in RMS])]

        # # Standard deviations

        yield [
            "mmv_std",
            tuple(
                [np.std(audio_slice[0]),
                 np.std(audio_slice[1]),
                 np.std(mono)])
        ]

        # # FFT shenanigans
        if do_calculate_fft:

            # The final fft we give to the shader
            processed = np.zeros(self.FFT_length, dtype=np.float32)

            # Counter to assign values on the processed array
            counter = 0

            # For each channel
            for channel_index, data in enumerate(audio_slice):

                # For every config dict on the config
                for info in self.process_layer_configs:

                    # Sample rate
                    original_sample_rate = info["original_sample_rate"]
                    target_sample_rate = info["target_sample_rate"]

                    # Individual frequencies
                    expected_frequencies = info["expected_frequencies"]

                    # The FFT of [[frequencies], [values]]
                    binned_fft = self.fourier.binned_fft(
                        data=self.resample(
                            data=data,
                            original_sample_rate=original_sample_rate,
                            target_sample_rate=target_sample_rate,
                        ),
                        original_sample_rate=original_sample_rate,
                        target_sample_rate=target_sample_rate,
                    )
                    if binned_fft is None: return

                    # Information on the frequencies, the index 0 is the DC bias, or frequency 0 Hz
                    # and at every index it jumps the distance between any index N and N+1
                    fft_freqs = binned_fft[0]
                    jumps = abs(fft_freqs[1])

                    # Get the nearest freq and add to processed
                    for freq in expected_frequencies:

                        # TODO: make configurable
                        flatten_scalar = self.functions.value_on_line_of_two_points(
                            Xa=20, Ya=0.1, Xb=20000, Yb=3, get_x=freq)

                        # # Get the nearest and FFT value

                        # Trick: since the jump of freqs are always the same, the nearest frequency index
                        # given a target freq will be the frequency itself divided by how many frequency we
                        # jump at the indexes
                        nearest = int(freq / jumps)

                        # The abs value of the FFT
                        value = abs(binned_fft[1][nearest]) * flatten_scalar

                        # Assign, iterate
                        processed[counter] = value
                        counter += 1

            # Yield FFT data
            yield ["mmv_fft", processed]

    # # Common Methods

    # Resample an audio slice (raw array) to some other frequency, this is useful when calculating
    # FFTs because a lower sample rate means we get more info on the bass freqs
    def resample(self, data: np.ndarray, original_sample_rate: int,
                 target_sample_rate: int) -> np.ndarray:

        # If the ratio is 1 then we don't do anything cause new/old = 1, just return the input data
        if target_sample_rate == original_sample_rate:
            return data

        # Use libsamplerate for resampling the audio otherwise
        return samplerate.resample(data,
                                   ratio=(target_sample_rate /
                                          original_sample_rate),
                                   converter_type='sinc_fastest')

    # Resample the data with nearest index approach
    # Doesn't really work, experimental, maybe I understand resampling wrong
    def resample_nearest(self, data: np.ndarray, original_sample_rate: int,
                         target_sample_rate: int) -> np.ndarray:

        # Nothing to do, target sample rate is the same as old one
        if (target_sample_rate == original_sample_rate):
            return data

        # The ratio we'll resample
        ratio = (original_sample_rate / target_sample_rate)

        # Length of the data
        N = data.shape[0]

        # Target new array length
        T = N * ratio

        # Array of original indexes
        indexes = np.arange(T)

        # (indexes / T) is the normalized to max at 1, we multiply
        # by the length of the original data so we expand the indexes
        # we just shaved by N * ratio, then use integer numbers and
        # offset by 1
        indexes = ((indexes / T) * N).astype(np.int32) - 1

        # Return the original data with selected indexes
        return data[indexes]

    # Get N semitones above / below A4 key, 440 Hz
    #
    # get_frequency_of_key(-12) = 220 Hz
    # get_frequency_of_key(  0) = 440 Hz
    # get_frequency_of_key( 12) = 880 Hz
    #
    def get_frequency_of_key(self, n, A4=440):
        return A4 * (2**(n / 12))

    # https://stackoverflow.com/a/2566508
    # Find nearest value inside one array from a given target value
    # I could make my own but this one is more efficient because it uses numpy
    # Returns: index of the match and its value
    def find_nearest(self, array, value):
        index = (np.abs(array - value)).argmin()
        return index, array[index]