Ableton but written in Rust
1use std::{f32::consts::PI, sync::Arc};
2
3use rodio::{OutputStream, Sink};
4use wgpu::util::TextureBlitter;
5use winit::{
6 application::ApplicationHandler,
7 error::EventLoopError,
8 event::WindowEvent,
9 event_loop::{ActiveEventLoop, ControlFlow, EventLoop},
10 window::Window,
11};
12use xilem::{
13 Color, WidgetView, WindowOptions, Xilem,
14 core::{Edit, ViewArgument, lens},
15 masonry::kurbo::{BezPath, Point, QuadSpline, Shape as _, Stroke},
16 style::{Padding, Style},
17 view::{
18 CrossAxisAlignment, MainAxisAlignment, flex_col, flex_row, label,
19 text_button,
20 },
21};
22
23use crate::clip::{Clip, ClipView};
24
25mod clip;
26
27struct AppState {
28 bpm: f64,
29 sample_rate: u32,
30 clips: Vec<Arc<Clip>>,
31 sink: Sink,
32 stream: OutputStream,
33}
34
35impl AppState {
36 pub fn new() -> Self {
37 let stream = rodio::OutputStreamBuilder::open_default_stream().unwrap();
38 let sample_rate = stream.config().sample_rate();
39
40 let sink = rodio::Sink::connect_new(stream.mixer());
41 sink.pause();
42
43 Self {
44 bpm: 120.0,
45 clips: vec![
46 Arc::new(Clip {
47 track: 0,
48 start: 0.0,
49 duration: 2.0,
50 color: Color::from_rgb8(200, 100, 100),
51 data: (0..sample_rate as usize * 2)
52 .map(|i| {
53 (2.0 * PI * 440.0 * (i as f32 / sample_rate as f32))
54 .sin() * 0.5
55 })
56 .collect(),
57 }),
58 Arc::new(Clip {
59 track: 1,
60 start: 1.0,
61 duration: 2.0,
62 color: Color::from_rgb8(100, 200, 100),
63 data: (0..sample_rate as usize * 2)
64 .map(|i| {
65 (2.0 * PI * 660.0 * (i as f32 / sample_rate as f32))
66 .sin() * 0.5
67 })
68 .collect(),
69 }),
70 ],
71 sample_rate,
72 sink,
73 stream,
74 }
75 }
76}
77
78fn clip_view(
79 state: &mut AppState,
80 index: usize,
81) -> impl WidgetView<Edit<AppState>> + use<> {
82 ClipView::new(state.clips[index].clone(), state.bpm, state.sample_rate)
83}
84
85fn header(state: &mut AppState) -> impl WidgetView<Edit<AppState>> + use<> {
86 flex_row((
87 label(format!("BPM: {}", state.bpm)),
88 text_button(
89 if state.sink.is_paused() {
90 "Play"
91 } else {
92 "Pause"
93 },
94 |state: &mut AppState| {
95 if state.sink.is_paused() {
96 state.sink.clear();
97 for clip in &state.clips {
98 let source = rodio::buffer::SamplesBuffer::new(
99 1,
100 state.sample_rate,
101 clip.data.clone(),
102 );
103 state.sink.append(source);
104 }
105 state.sink.play();
106 } else {
107 state.sink.pause();
108 }
109 },
110 ),
111 ))
112 .main_axis_alignment(MainAxisAlignment::Start)
113 .cross_axis_alignment(CrossAxisAlignment::Center)
114}
115
116fn app_logic(state: &mut AppState) -> impl WidgetView<Edit<AppState>> + use<> {
117 flex_col((
118 header(state),
119 (0..state.clips.len())
120 .map(|i| clip_view(state, i))
121 .collect::<Vec<_>>(),
122 ))
123 .main_axis_alignment(MainAxisAlignment::Start)
124 .cross_axis_alignment(CrossAxisAlignment::Start)
125 .padding(Padding::all(10.0))
126}
127
128fn main() -> Result<(), EventLoopError> {
129 tracing_subscriber::fmt::init();
130
131 let app = Xilem::new_simple(
132 AppState::new(),
133 app_logic,
134 WindowOptions::new("Ableton Clone"),
135 );
136 app.run_in(EventLoop::with_user_event())?;
137 Ok(())
138}