diff --git a/core/actions.py b/core/action.py similarity index 86% rename from core/actions.py rename to core/action.py index 270f9ea..a05cf78 100644 --- a/core/actions.py +++ b/core/action.py @@ -26,7 +26,7 @@ class TimedAction: self.end(node) else: - self.progress(t - self.start_t, node) + self.progress((t - self.start_t) / float(self.duration_t), node) # REIMPLEMENT @@ -38,6 +38,6 @@ class TimedAction: pass - def progress(self, rel_t, node): + def progress(self, prc, node): pass \ No newline at end of file diff --git a/core/actions/basics.py b/core/actions/basics.py new file mode 100644 index 0000000..5287812 --- /dev/null +++ b/core/actions/basics.py @@ -0,0 +1,82 @@ +from ..action import TimedAction + +class PlayAction(TimedAction): + + def __init__(self, start_t, duration_t, nodes, program): + + super().__init__(start_t, duration_t, nodes, program) + + def start(self, node): + + node.active = True + + def end(self, node): + + node.active = False + + +class NoteAction(TimedAction): + + def __init__(self, note, start_t, duration_t, nodes, program): + + super().__init__(start_t, duration_t, nodes, program) + self.note = note + + def start(self, node): + + node.freq = self.program.note_to_freq(self.note) + node.active = True + + def end(self, node): + + node.active = False + +class LinearPitchTransition(TimedAction): + + def __init__(self, start_note, end_node, start_t, duration_t, nodes, program): + + super().__init__(start_t, duration_t, nodes, program) + self.start_note = start_note + self.end_node = end_node + + self.start_freq = self.program.note_to_freq(start_note) + self.end_freq = self.program.note_to_freq(end_node) + + self.freq_range = self.end_freq - self.start_freq + + def start(self, node): + + node.freq = self.start_freq + node.active = True + + def progress(self, prc, node): + + node.freq = self.start_freq + (self.freq_range * prc) + + def end(self, node): + + node.active = False + + +class LinearSpatialTransition(TimedAction): + + def __init__(self, start_coords, end_coords, start_t, duration_t, nodes, program): + + super().__init__(start_t, duration_t, nodes, program) + self.start_coords = start_coords + self.end_coords = end_coords + + self.delta_coords = [end_coords[0] - start_coords[0], end_coords[1] - start_coords[1], end_coords[2] - start_coords[2]] + + def start(self, node): + + node.start_location = self.start_coords + + def progress(self, prc, node): + + l = [] + l.append(self.start_coords[0] + (self.delta_coords[0] * prc)) + l.append(self.start_coords[1] + (self.delta_coords[1] * prc)) + l.append(self.start_coords[2] + (self.delta_coords[2] * prc)) + node.start_location = tuple(l) + \ No newline at end of file diff --git a/core/nodes/sawtooth.py b/core/nodes/sawtooth.py new file mode 100644 index 0000000..24ad6ce --- /dev/null +++ b/core/nodes/sawtooth.py @@ -0,0 +1,27 @@ +import math + +from ..soundnode import SoundNode + +class SawtoothNode(SoundNode): + + def __init__(self, freq, room): + + super().__init__("saw", room) + self.freq = freq + self.harmonics_q = 50 + self.active = False + self.volume = 0.5 + + def calc_r_amps(self, t): + + if not self.active: + self.r_amps[t] = dict() + return + + if not t in self.r_amps: + self.r_amps[t] = dict() + + for i in range(0, self.harmonics_q): + + freq = self.freq*(i + 1) + self.r_amps[t][freq] = self.volume / float(i+1) \ No newline at end of file diff --git a/core/nodes/sinenode.py b/core/nodes/sinenode.py index 598d021..3e8f558 100644 --- a/core/nodes/sinenode.py +++ b/core/nodes/sinenode.py @@ -1,14 +1,13 @@ import math from ..soundnode import SoundNode -from ..actions import TimedAction class SineNode(SoundNode): - def __init__(self, freqs, room): + def __init__(self, freq, room): super().__init__("sine", room) - self.freqs = freqs + self.freq = freq self.active = False self.volume = 1 @@ -17,44 +16,5 @@ class SineNode(SoundNode): if not self.active: self.r_amps[t] = dict() return - - tdct = dict() - for freq in self.freqs: - tdct[freq] = self.volume * math.sin(self.room.sine_multiplier * freq * t) - - self.r_amps[t] = tdct - -### - -class PlayAction(TimedAction): - - def __init__(self, start_t, duration_t, nodes, program): - - super().__init__(start_t, duration_t, nodes, program) - - def start(self, node): - - node.active = True - - def end(self, node): - - node.active = False - - -class NoteAction(TimedAction): - - def __init__(self, note, start_t, duration_t, nodes, program): - - super().__init__(start_t, duration_t, nodes, program) - self.note = note - - def start(self, node): - - note_freq = self.program.note_to_freq(self.note) - node.freqs = [note_freq] - node.active = True - - def end(self, node): - - node.active = False \ No newline at end of file + self.r_amps[t] = {self.freq: self.volume} diff --git a/core/nodes/sink.py b/core/nodes/sink.py index 16208a5..97c0c9b 100644 --- a/core/nodes/sink.py +++ b/core/nodes/sink.py @@ -26,4 +26,7 @@ class SinkNode(SoundNode): res[freq] = vol else: res[freq] += vol - self.r_amps[t] = res \ No newline at end of file + + self.r_amps[t] = res + #if res: + # print(self.r_amps[t]) \ No newline at end of file diff --git a/core/nodes/triangle.py b/core/nodes/triangle.py new file mode 100644 index 0000000..af7542b --- /dev/null +++ b/core/nodes/triangle.py @@ -0,0 +1,33 @@ +import math + +from ..soundnode import SoundNode + +class TriangleNode(SoundNode): + + def __init__(self, freq, room): + + super().__init__("trg", room) + self.freq = freq + self.harmonics_q = 30 + self.active = False + self.volume = 0.8 + + def calc_r_amps(self, t): + + if not self.active: + self.r_amps[t] = dict() + return + + if not t in self.r_amps: + self.r_amps[t] = dict() + + for i in range(0, self.harmonics_q): + + freq = self.freq*(i*2 + 1) + + if i%2 == 0: + m = 1 + else: + m = -1 + + self.r_amps[t][freq] = m * self.volume / float((i*2 + 1)**2) \ No newline at end of file diff --git a/core/program.py b/core/program.py index d65010c..c1589a7 100644 --- a/core/program.py +++ b/core/program.py @@ -1,8 +1,14 @@ import os import wave +import math from .room import Room +TAU = 2 * math.pi + +def stb(i: int, bytelen: int) -> bytes: + return i.to_bytes(bytelen, byteorder='little', signed=True) + class Program: @@ -66,13 +72,75 @@ class Program: start_t = int(start_t_sec * self.room.sample_rate) end_t = int(end_t_sec * self.room.sample_rate) + order_of_passage = [] + for t in range(start_t, end_t): + if t%1000 == 0: + print(f'{int(t / float(end_t - start_t) * 100)}%', end='\r') + self.tick(t) - left_frame, right_frame = self.room.generate_frame(t) + + for node in self.room.nodes: + node.tick_done = False + + updates = True + + if not order_of_passage: + + while updates: + + updates = False + + for node in self.room.nodes: + + if not (t in node.r_amps): + + has_unprocessed_inputs = False + + for in_node in node.air_in: + if not in_node.tick_done: + has_unprocessed_inputs = True + break + + if not has_unprocessed_inputs: + for in_node in node.wire_in: + if not in_node.tick_done: + has_unprocessed_inputs = True + break + + if not has_unprocessed_inputs: + + node.calc_r_amps(t) + node.tick_done = True + updates = True + order_of_passage.append(node) + + else: + for node in order_of_passage: + node.calc_r_amps(t) + + left_total_amp = self.room.max_amp * sum([vol * math.sin(self.room.sine_multiplier * freq * t) for freq, vol in self.room.left_sink.r_amps[t].items()]) + right_total_amp = self.room.max_amp * sum([vol * math.sin(self.room.sine_multiplier * freq * t) for freq, vol in self.room.right_sink.r_amps[t].items()]) + + if left_total_amp > self.room.max_amp: + left_total_amp = self.room.max_amp + + if left_total_amp < self.room.min_amp: + left_total_amp = self.room.min_amp + + if right_total_amp > self.room.max_amp: + right_total_amp = self.room.max_amp + + if right_total_amp < self.room.min_amp: + right_total_amp = self.room.min_amp + + + left_frame, right_frame = stb(int(left_total_amp), self.room.sample_width), stb(int(right_total_amp), self.room.sample_width) frames.append(left_frame) frames.append(right_frame) + print('100%', end='\r') return frames @@ -105,11 +173,13 @@ class Program: cmd = '' while cmd != 'q': - cmd = input('\n[vol {self.volume*100}%]>> ') + cmd = input(f'\n[vol {self.volume*100}%]>> ') if cmd.startswith('g '): + import cProfile end_sec = float(cmd[2:]) - self.export(0, end_sec) + cProfile.runctx("self.export(0, end_sec)", globals(), locals()) + #self.export(0, end_sec) if cmd.startswith('vol '): vol = float(cmd[4:]) diff --git a/core/room.py b/core/room.py index 50049dd..80f235f 100644 --- a/core/room.py +++ b/core/room.py @@ -42,40 +42,46 @@ class Room: for node in self.nodes: node.tick_done = False - if t%1000 == 0: - print(t, end='\r', flush=True) - updates = True + order_of_passage = [] - while updates: + if not order_of_passage: - updates = False - - for node in self.nodes: + while updates: - if not (t in node.r_amps): + updates = False + + for node in self.nodes: - has_unprocessed_inputs = False - - for in_node in node.air_in: - if not in_node.tick_done: - has_unprocessed_inputs = True - break - - if not has_unprocessed_inputs: - for in_node in node.wire_in: + if not (t in node.r_amps): + + has_unprocessed_inputs = False + + for in_node in node.air_in: if not in_node.tick_done: has_unprocessed_inputs = True break - - if not has_unprocessed_inputs: - node.calc_r_amps(t) - node.tick_done = True - updates = True + if not has_unprocessed_inputs: + for in_node in node.wire_in: + if not in_node.tick_done: + has_unprocessed_inputs = True + break + + if not has_unprocessed_inputs: + + node.calc_r_amps(t) + node.tick_done = True + updates = True + order_of_passage.append(node) + + + else: + for node in order_of_passage: + node.calc_r_amps(t) - left_total_amp = self.max_amp * sum([vol for freq, vol in self.left_sink.r_amps[t].items()]) - right_total_amp = self.max_amp * sum([vol for freq, vol in self.right_sink.r_amps[t].items()]) + left_total_amp = self.max_amp * sum([vol * math.sin(self.sine_multiplier * freq * t) for freq, vol in self.left_sink.r_amps[t].items()]) + right_total_amp = self.max_amp * sum([vol * math.sin(self.sine_multiplier * freq * t) for freq, vol in self.right_sink.r_amps[t].items()]) if left_total_amp > self.max_amp: left_total_amp = self.max_amp diff --git a/core/soundnode.py b/core/soundnode.py index 72e0831..8f3986a 100644 --- a/core/soundnode.py +++ b/core/soundnode.py @@ -24,7 +24,10 @@ class SoundNode: loc = self.location(t) other_loc = other_node.location(t) - + diff_x = loc[0]-other_loc[0] + diff_y = loc[1]-other_loc[1] + diff_z = loc[2]-other_loc[2] + return diff_x*diff_x + diff_y*diff_y + diff_z * diff_z return (loc[0]-other_loc[0])**2 + (loc[1]-other_loc[1])**2 + (loc[2]-other_loc[2])**2 def sample_r_amps_by_wire(self, source_node, current_t): @@ -40,7 +43,9 @@ class SoundNode: sample_t = current_t - int(dist / self.room.speed_of_sound) if sample_t in source_node.r_amps: - return {f: a / float(dist) for f, a in source_node.r_amps[sample_t].items()} + + attenuation = math.exp(-(dist/100.0)) + return {f: a * attenuation for f, a in source_node.r_amps[sample_t].items()} return dict() diff --git a/test.py b/test.py index 453bc56..1f29b34 100644 --- a/test.py +++ b/test.py @@ -4,6 +4,9 @@ from core.room import Room from core.program import Program from core.soundnode import SoundNode from core.nodes.sinenode import * +from core.nodes.triangle import * +from core.nodes.sawtooth import * +from core.actions.basics import * class TestProgram(Program): @@ -12,31 +15,30 @@ class TestProgram(Program): super().__init__("testprogram") def setup(self): - self.reset() - sn = SineNode([], self.room) + sn = SineNode(440, self.room) + sn2 = SawtoothNode(440, self.room) sn.air_to(self.room.left_sink) - #sn.air_to(self.room.right_sink) - - NoteAction('A4', self.st(0), self.st(0.5), [sn], self) - NoteAction('G4', self.st(1), self.st(0.5), [sn], self) - NoteAction('F4', self.st(2), self.st(0.5), [sn], self) - NoteAction('E4', self.st(3), self.st(0.5), [sn], self) - - sn2 = SineNode([], self.room) - - #sn2.air_to(self.room.left_sink) + sn.air_to(self.room.right_sink) + sn2.air_to(self.room.left_sink) sn2.air_to(self.room.right_sink) - NoteAction('A3', self.st(0), self.st(0.5), [sn2], self) - NoteAction('G3', self.st(1), self.st(0.5), [sn2], self) - NoteAction('F3', self.st(2), self.st(0.5), [sn2], self) - NoteAction('E3', self.st(3), self.st(0.5), [sn2], self) - - + sn.start_location = (4,0,0) + sn2.start_location = (-4,0,0) + + """ + NoteAction('A4', self.st(0), self.st(0.5), [sn, tn], self) + NoteAction('G4', self.st(1), self.st(0.5), [sn, tn], self) + NoteAction('F4', self.st(2), self.st(0.5), [sn, tn], self) + NoteAction('E4', self.st(3), self.st(0.5), [sn, tn], self) + """ + LinearPitchTransition('A4', 'E4', self.st(0), self.st(5), [sn], self) + LinearPitchTransition('E4', 'A4', self.st(0), self.st(5), [sn2], self) + LinearSpatialTransition((-8,0,0),(8,0,0), self.st(0), self.st(5), [sn], self) + TP = TestProgram() TP.setup() TP.interface() \ No newline at end of file