diff --git a/model/visulization/AudioAnalyzer.py b/model/visulization/AudioAnalyzer.py new file mode 100644 index 0000000..93f0986 --- /dev/null +++ b/model/visulization/AudioAnalyzer.py @@ -0,0 +1,221 @@ +import math + +import matplotlib.pyplot as plt +import librosa.display +import numpy as np + + +# binary search +import pygame + + +def bin_search(arr, target): + index = int(len(arr) / 2) + min_index = 0 + max_index = len(arr) - 1 + found = False + + if target < arr[0]: + return 0 + + if target > arr[len(arr) - 1]: + return len(arr) - 1 + + while not found: + + if min_index == len(arr) - 2: + return len(arr) - 1 + + if arr[index] < target < arr[index + 1] or arr[index] == target: + return index + + if arr[index] > target: + max_index = index + else: + min_index = index + + index = int((min_index + max_index) / 2) + +def rotate(xy, theta): + # https://en.wikipedia.org/wiki/Rotation_matrix#In_two_dimensions + cos_theta, sin_theta = math.cos(theta), math.sin(theta) + + return ( + xy[0] * cos_theta - xy[1] * sin_theta, + xy[0] * sin_theta + xy[1] * cos_theta + ) + + +def translate(xy, offset): + return xy[0] + offset[0], xy[1] + offset[1] + + +def clamp(min_value, max_value, value): + + if value < min_value: + return min_value + + if value > max_value: + return max_value + + return value + + +class AudioAnalyzer: + + def __init__(self): + + self.frequencies_index_ratio = 0 # array for frequencies + self.time_index_ratio = 0 # array of time periods + self.spectrogram = None # a matrix that contains decibel values according to frequency and time indexes + + def load(self, filename): + + time_series, sample_rate = librosa.load(filename) # getting information from the file + + # getting a matrix which contains amplitude values according to frequency and time indexes + stft = np.abs(librosa.stft(time_series, hop_length=512, n_fft=2048*4)) + + self.spectrogram = librosa.amplitude_to_db(stft, ref=np.max) # converting the matrix to decibel matrix + + frequencies = librosa.core.fft_frequencies(n_fft=2048*4) # getting an array of frequencies + + # getting an array of time periodic + times = librosa.core.frames_to_time(np.arange(self.spectrogram.shape[1]), sr=sample_rate, hop_length=512, n_fft=2048*4) + + self.time_index_ratio = len(times)/times[len(times) - 1] + + self.frequencies_index_ratio = len(frequencies)/frequencies[len(frequencies)-1] + + + + + def show(self): + + librosa.display.specshow(self.spectrogram, + y_axis='log', x_axis='time') + + plt.title('spectrogram') + plt.colorbar(format='%+2.0f dB') + plt.tight_layout() + plt.show() + + def get_decibel(self, target_time, freq): + + return self.spectrogram[int(freq*self.frequencies_index_ratio)][int(target_time*self.time_index_ratio)] + + # returning the current decibel according to the indexes which found by binary search + # return self.spectrogram[bin_search(self.frequencies, freq), bin_search(self.times, target_time)] + + def get_decibel_array(self, target_time, freq_arr): + + arr = [] + + for f in freq_arr: + arr.append(self.get_decibel(target_time,f)) + + return arr + + +class AudioBar: + + def __init__(self, x, y, freq, color, width=50, min_height=10, max_height=100, min_decibel=-80, max_decibel=0): + + self.x, self.y, self.freq = x, y, freq + + self.color = color + + self.width, self.min_height, self.max_height = width, min_height, max_height + + self.height = min_height + + self.min_decibel, self.max_decibel = min_decibel, max_decibel + + self.__decibel_height_ratio = (self.max_height - self.min_height)/(self.max_decibel - self.min_decibel) + + def update(self, dt, decibel): + + desired_height = decibel * self.__decibel_height_ratio + self.max_height + + speed = (desired_height - self.height)/0.1 + + self.height += speed * dt + + self.height = clamp(self.min_height, self.max_height, self.height) + + def render(self, screen): + + pygame.draw.rect(screen, self.color, (self.x, self.y + self.max_height - self.height, self.width, self.height)) + + +class AverageAudioBar(AudioBar): + + def __init__(self, x, y, rng, color, width=50, min_height=10, max_height=100, min_decibel=-80, max_decibel=0): + super().__init__(x, y, 0, color, width, min_height, max_height, min_decibel, max_decibel) + + self.rng = rng + + self.avg = 0 + + def update_all(self, dt, time, analyzer): + + self.avg = 0 + + for i in self.rng: + self.avg += analyzer.get_decibel(time, i) + + self.avg /= len(self.rng) + self.update(dt, self.avg) + + +class RotatedAverageAudioBar(AverageAudioBar): + + def __init__(self, x, y, rng, color, angle=0, width=50, min_height=10, max_height=100, min_decibel=-80, max_decibel=0): + super().__init__(x, y, 0, color, width, min_height, max_height, min_decibel, max_decibel) + + self.rng = rng + + self.rect = None + + self.angle = angle + + + def render(self, screen): + + pygame.draw.polygon(screen, self.color, self.rect.points) + + def render_c(self, screen, color): + + pygame.draw.polygon(screen, color, self.rect.points) + + def update_rect(self): + self.rect = Rect(self.x, self.y, self.width, self.height) + + self.rect.rotate(self.angle) + + +class Rect: + + def __init__(self,x ,y, w, h): + self.x, self.y, self.w, self.h = x,y, w, h + + self.points = [] + + self.origin = [self.w/2,0] + self.offset = [self.origin[0] + x, self.origin[1] + y] + + self.rotate(0) + + def rotate(self, angle): + + template = [ + (-self.origin[0], self.origin[1]), + (-self.origin[0] + self.w, self.origin[1]), + (-self.origin[0] + self.w, self.origin[1] - self.h), + (-self.origin[0], self.origin[1] - self.h) + ] + + self.points = [translate(rotate(xy, math.radians(angle)), self.offset) for xy in template] + + def draw(self,screen): + pygame.draw.polygon(screen, (255,255, 0), self.points) diff --git a/model/visulization/main.py b/model/visulization/main.py new file mode 100644 index 0000000..33b090e --- /dev/null +++ b/model/visulization/main.py @@ -0,0 +1,194 @@ +from AudioAnalyzer import * +import random +import colorsys + +#filename = "团子伴奏提取_Ryan+Gosling,Emma+Stone+-+City+Of+Stars+(From+"La+La+Land"+Soundtrack)_伴奏.mp3" +filename = 'Ryan Gosling,Emma Stone - City Of Stars (From "La La Land" Soundtrack).mp3' +def rnd_color(): + h, s, l = random.random(), 0.5 + random.random() / 2.0, 0.4 + random.random() / 5.0 + return [int(256 * i) for i in colorsys.hls_to_rgb(h, l, s)] + +analyzer = AudioAnalyzer() +analyzer.load(filename) + +pygame.init() + +infoObject = pygame.display.Info() + +screen_w = int(infoObject.current_w/3) +screen_h = int(infoObject.current_w/3) + +# Set up the drawing window +screen = pygame.display.set_mode([screen_w, screen_h]) + + +t = pygame.time.get_ticks() +getTicksLastFrame = t + +timeCount = 0 + +avg_bass = 0 +bass_trigger = -30 +bass_trigger_started = 0 + +min_decibel = -80 +max_decibel = 80 + +circle_color = (40, 40, 40) +polygon_default_color = [255, 255, 255] +polygon_bass_color = polygon_default_color.copy() +polygon_color_vel = [0, 0, 0] + +poly = [] +poly_color = polygon_default_color.copy() + +circleX = int(screen_w / 2) +circleY = int(screen_h/2) + +min_radius = 100 +max_radius = 150 +radius = min_radius +radius_vel = 0 + + +bass = {"start": 50, "stop": 100, "count": 12} +heavy_area = {"start": 120, "stop": 250, "count": 40} +low_mids = {"start": 251, "stop": 2000, "count": 50} +high_mids = {"start": 2001, "stop": 6000, "count": 20} + +freq_groups = [bass, heavy_area, low_mids, high_mids] + + +bars = [] + +tmp_bars = [] + + +length = 0 + +for group in freq_groups: + + g = [] + + s = group["stop"] - group["start"] + + count = group["count"] + + reminder = s%count + + step = int(s/count) + + rng = group["start"] + + for i in range(count): + + arr = None + + if reminder > 0: + reminder -= 1 + arr = np.arange(start=rng, stop=rng + step + 2) + rng += step + 3 + else: + arr = np.arange(start=rng, stop=rng + step + 1) + rng += step + 2 + + g.append(arr) + + length += 1 + + tmp_bars.append(g) + + +angle_dt = 360/length + +ang = 0 + +for g in tmp_bars: + gr = [] + for c in g: + gr.append( + RotatedAverageAudioBar(circleX+radius*math.cos(math.radians(ang - 90)), circleY+radius*math.sin(math.radians(ang - 90)), c, (255, 0, 255), angle=ang, width=8, max_height=370)) + ang += angle_dt + + bars.append(gr) + + +pygame.mixer.music.load(filename) +pygame.mixer.music.play(0) + +running = True +while running: + + avg_bass = 0 + poly = [] + + # ticks + t = pygame.time.get_ticks() + deltaTime = (t - getTicksLastFrame) / 1000.0 + getTicksLastFrame = t + + timeCount += deltaTime + + screen.fill(circle_color) + + for event in pygame.event.get(): + if event.type == pygame.QUIT: + running = False + + for b1 in bars: + for b in b1: + b.update_all(deltaTime, pygame.mixer.music.get_pos() / 1000.0, analyzer) + + for b in bars[0]: + avg_bass += b.avg + + avg_bass /= len(bars[0]) + + if avg_bass > bass_trigger: + if bass_trigger_started == 0: + bass_trigger_started = pygame.time.get_ticks() + if (pygame.time.get_ticks() - bass_trigger_started)/1000.0 > 2: + polygon_bass_color = rnd_color() + bass_trigger_started = 0 + if polygon_bass_color is None: + polygon_bass_color = rnd_color() + newr = min_radius + int(avg_bass * ((max_radius - min_radius) / (max_decibel - min_decibel)) + (max_radius - min_radius)) + radius_vel = (newr - radius) / 0.15 + + polygon_color_vel = [(polygon_bass_color[x] - poly_color[x])/0.15 for x in range(len(poly_color))] + + elif radius > min_radius: + bass_trigger_started = 0 + polygon_bass_color = None + radius_vel = (min_radius - radius) / 0.15 + polygon_color_vel = [(polygon_default_color[x] - poly_color[x])/0.15 for x in range(len(poly_color))] + + else: + bass_trigger_started = 0 + poly_color = polygon_default_color.copy() + polygon_bass_color = None + polygon_color_vel = [0, 0, 0] + + radius_vel = 0 + radius = min_radius + + radius += radius_vel * deltaTime + + for x in range(len(polygon_color_vel)): + value = polygon_color_vel[x]*deltaTime + poly_color[x] + poly_color[x] = value + + for b1 in bars: + for b in b1: + b.x, b.y = circleX+radius*math.cos(math.radians(b.angle - 90)), circleY+radius*math.sin(math.radians(b.angle - 90)) + b.update_rect() + + poly.append(b.rect.points[3]) + poly.append(b.rect.points[2]) + + pygame.draw.polygon(screen, poly_color, poly) + pygame.draw.circle(screen, circle_color, (circleX, circleY), int(radius)) + + pygame.display.flip() + +pygame.quit()