tangled
alpha
login
or
join now
inkreas.ing
/
torque-tracker-engine
0
fork
atom
old school music tracker audio backend
0
fork
atom
overview
issues
pulls
pipelines
change AudioState communication
luca3s
1 year ago
09158883
f39aaee0
+79
-97
7 changed files
expand all
collapse all
unified
split
Cargo.lock
tracker-engine
Cargo.toml
examples
live_note.rs
pattern_playback.rs
src
audio_processing
playback.rs
live_audio.rs
manager.rs
+10
Cargo.lock
···
364
364
"hound",
365
365
"rtrb",
366
366
"simple-left-right",
367
367
+
"triple_buffer",
367
368
]
368
369
369
370
[[package]]
···
788
789
version = "0.1.32"
789
790
source = "registry+https://github.com/rust-lang/crates.io-index"
790
791
checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
792
792
+
793
793
+
[[package]]
794
794
+
name = "triple_buffer"
795
795
+
version = "8.0.0"
796
796
+
source = "registry+https://github.com/rust-lang/crates.io-index"
797
797
+
checksum = "9e66931c8eca6381f0d34656a9341f09bd462010488c1a3bc0acd3f2d08dffce"
798
798
+
dependencies = [
799
799
+
"crossbeam-utils",
800
800
+
]
791
801
792
802
[[package]]
793
803
name = "unicode-ident"
+1
tracker-engine/Cargo.toml
···
25
25
crossbeam-utils = "0.8.20"
26
26
rtrb = "0.3.1"
27
27
simple-left-right = { path = "../simple-left-right" }
28
28
+
triple_buffer = "8.0.0"
28
29
# assert_no_alloc
29
30
30
31
[dev-dependencies]
+3
-15
tracker-engine/examples/live_note.rs
···
3
3
use cpal::{traits::DeviceTrait, Sample};
4
4
use impulse_engine::{
5
5
live_audio::ToWorkerMsg,
6
6
-
manager::{AudioManager, AudioMsgConfig, OutputConfig},
6
6
+
manager::{AudioManager, OutputConfig},
7
7
project::{
8
8
event_command::NoteCommand,
9
9
note_event::{Note, NoteEvent, VolumeEffect},
···
43
43
sample_rate: default_config.sample_rate().0,
44
44
};
45
45
46
46
-
let mut recv = manager
47
47
-
.init_audio(
48
48
-
default_device,
49
49
-
config,
50
50
-
AudioMsgConfig {
51
51
-
buffer_finished: true,
52
52
-
..Default::default()
53
53
-
},
54
54
-
20,
55
55
-
)
56
56
-
.unwrap();
46
46
+
let mut recv = manager.init_audio(default_device, config).unwrap();
57
47
58
48
let note_event = NoteEvent {
59
49
note: Note::new(90).unwrap(),
···
65
55
std::thread::sleep(Duration::from_secs(1));
66
56
manager.send_worker_msg(ToWorkerMsg::PlayEvent(note_event));
67
57
std::thread::sleep(Duration::from_secs(1));
68
68
-
while let Ok(event) = recv.pop() {
69
69
-
println!("{event:?}");
70
70
-
}
58
58
+
println!("{:?}", recv.read());
71
59
}
+3
-15
tracker-engine/examples/pattern_playback.rs
···
3
3
use cpal::{traits::DeviceTrait, Sample};
4
4
use impulse_engine::{
5
5
live_audio::ToWorkerMsg,
6
6
-
manager::{AudioManager, AudioMsgConfig, OutputConfig, PlaybackSettings},
6
6
+
manager::{AudioManager, OutputConfig, PlaybackSettings},
7
7
project::{
8
8
event_command::NoteCommand,
9
9
note_event::{Note, NoteEvent, VolumeEffect},
···
68
68
sample_rate: default_config.sample_rate().0,
69
69
};
70
70
71
71
-
let mut recv = manager
72
72
-
.init_audio(
73
73
-
default_device,
74
74
-
config,
75
75
-
AudioMsgConfig {
76
76
-
playback_position: true,
77
77
-
..Default::default()
78
78
-
},
79
79
-
20,
80
80
-
)
81
81
-
.unwrap();
71
71
+
let mut recv = manager.init_audio(default_device, config).unwrap();
82
72
83
73
manager.send_worker_msg(ToWorkerMsg::Playback(PlaybackSettings::default()));
84
74
85
75
std::thread::sleep(Duration::from_secs(5));
86
76
manager.deinit_audio();
87
87
-
// while let Ok(event) = recv.try_next() {
88
88
-
// println!("{event:?}");
89
89
-
// }
77
77
+
println!("{:?}", recv.read())
90
78
}
+13
-12
tracker-engine/src/audio_processing/playback.rs
···
6
6
project::song::Song,
7
7
};
8
8
9
9
-
// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
10
10
-
// pub struct PlaybackPosition {
11
11
-
// pub order: usize,
12
12
-
// pub pattern: usize,
13
13
-
// pub row: u16,
14
14
-
// }
9
9
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10
10
+
pub struct PlaybackStatus {
11
11
+
position: PlaybackPosition,
12
12
+
// which sample is playing,
13
13
+
// which how far along is each sample
14
14
+
// which channel is playing
15
15
+
// ...
16
16
+
}
15
17
16
18
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17
19
pub struct PlaybackPosition {
···
122
124
(samplerate * 10) / u32::from(tempo)
123
125
}
124
126
125
125
-
pub fn get_position(&self) -> PlaybackPosition {
126
126
-
self.position
127
127
+
pub fn get_status(&self) -> PlaybackStatus {
128
128
+
// maybe if it gets more fields compute them while playing back and just copy out here
129
129
+
PlaybackStatus {
130
130
+
position: self.position,
131
131
+
}
127
132
}
128
133
129
134
pub fn set_samplerate(&mut self, samplerate: u32) {
···
238
243
false
239
244
}
240
245
}
241
241
-
}
242
242
-
243
243
-
pub fn get_position(&self) -> PlaybackPosition {
244
244
-
self.state.get_position()
245
246
}
246
247
}
247
248
+19
-23
tracker-engine/src/live_audio.rs
···
1
1
use std::fmt::Debug;
2
2
use std::ops::{AddAssign, IndexMut};
3
3
4
4
-
use crate::audio_processing::playback::PlaybackState;
4
4
+
use crate::audio_processing::playback::{PlaybackState, PlaybackStatus};
5
5
use crate::audio_processing::sample::Interpolation;
6
6
use crate::audio_processing::sample::SamplePlayer;
7
7
use crate::audio_processing::Frame;
8
8
-
use crate::manager::{AudioMsgConfig, FromWorkerMsg, OutputConfig, PlaybackSettings};
8
8
+
use crate::manager::{OutputConfig, PlaybackSettings};
9
9
use crate::project::note_event::NoteEvent;
10
10
use crate::project::song::Song;
11
11
use cpal::{Sample, SampleFormat};
12
12
use simple_left_right::Reader;
13
13
+
14
14
+
pub type LiveAudioStatus = (Option<PlaybackStatus>, cpal::OutputStreamTimestamp);
13
15
14
16
pub(crate) struct LiveAudio {
15
17
song: Reader<Song<true>>,
16
18
playback_state: Option<PlaybackState<'static, true>>,
17
19
live_note: Option<SamplePlayer<'static, true>>,
18
18
-
// replace with something explicitly realtime safe. I think std mpsc does syscalls to sleep and wake the thread
19
20
manager: rtrb::Consumer<ToWorkerMsg>,
20
20
-
audio_msg_config: AudioMsgConfig,
21
21
-
to_app: rtrb::Producer<FromWorkerMsg>,
21
21
+
// gets created in the first callback. could maybe do with an MaybeUninit
22
22
+
state_sender: triple_buffer::Input<Option<LiveAudioStatus>>,
22
23
config: OutputConfig,
24
24
+
23
25
buffer: Box<[Frame]>,
24
26
}
25
27
···
30
32
pub fn new(
31
33
song: Reader<Song<true>>,
32
34
manager: rtrb::Consumer<ToWorkerMsg>,
33
33
-
audio_msg_config: AudioMsgConfig,
34
34
-
to_app: rtrb::Producer<FromWorkerMsg>,
35
35
+
state_sender: triple_buffer::Input<Option<LiveAudioStatus>>,
35
36
config: OutputConfig,
36
37
) -> Self {
37
38
Self {
···
39
40
playback_state: None,
40
41
live_note: None,
41
42
manager,
42
42
-
audio_msg_config,
43
43
-
to_app,
43
43
+
state_sender,
44
44
config,
45
45
buffer: vec![Frame::default(); config.buffer_size.try_into().unwrap()].into(),
46
46
}
47
47
}
48
48
49
49
+
fn send_state(&mut self, info: &cpal::OutputCallbackInfo) {
50
50
+
self.state_sender.write(Some((
51
51
+
self.playback_state.as_ref().map(|s| s.get_status()),
52
52
+
info.timestamp(),
53
53
+
)));
54
54
+
}
55
55
+
49
56
#[inline]
50
57
/// returns true if work was done
51
58
fn fill_internal_buffer(&mut self) -> bool {
···
97
104
98
105
// process song playback
99
106
if let Some(playback) = &mut self.playback_state {
100
100
-
let old_position = playback.get_position();
101
107
let playback_iter = playback.iter::<{ Self::INTERPOLATION }>(&song);
102
108
self.buffer
103
109
.iter_mut()
104
110
.zip(playback_iter)
105
111
.for_each(|(buf, frame)| buf.add_assign(frame));
106
106
-
107
107
-
if self.audio_msg_config.playback_position && old_position != playback.get_position() {
108
108
-
let _ = self.to_app.push(FromWorkerMsg::CurrentPlaybackPosition(
109
109
-
playback.get_position(),
110
110
-
));
111
111
-
}
112
112
113
113
if playback.is_done() {
114
114
self.playback_state = None;
···
172
172
_ => panic!("Sample Format not supported."),
173
173
}
174
174
175
175
-
if self.audio_msg_config.buffer_finished {
176
176
-
let _ = self
177
177
-
.to_app
178
178
-
.push(FromWorkerMsg::BufferFinished(info.timestamp()));
179
179
-
}
175
175
+
self.send_state(info);
180
176
}
181
177
}
182
178
183
179
// unsure wether i want to use this or untyped_callback
184
180
// also relevant when cpal gets made into a generic that maybe this gets useful
185
185
-
#[expect(dead_code)]
186
181
pub fn get_typed_callback<S: cpal::SizedSample + cpal::FromSample<f32>>(
187
182
mut self,
188
183
) -> impl FnMut(&mut [S], &cpal::OutputCallbackInfo) {
189
189
-
move |data, _info| {
184
184
+
move |data, info| {
190
185
assert_eq!(
191
186
data.len(),
192
187
usize::try_from(self.config.buffer_size).unwrap()
···
196
191
if self.fill_internal_buffer() {
197
192
self.fill_from_internal(data);
198
193
}
194
194
+
self.send_state(info);
199
195
}
200
196
}
201
197
}
+30
-32
tracker-engine/src/manager.rs
···
1
1
-
use std::{
2
2
-
fmt::Debug, mem::ManuallyDrop, num::NonZeroU16, time::Duration
3
3
-
};
1
1
+
use std::{fmt::Debug, mem::ManuallyDrop, num::NonZeroU16, time::Duration};
4
2
5
3
#[cfg(feature = "async")]
6
4
use std::ops::ControlFlow;
···
11
9
12
10
use crate::{
13
11
audio_processing::playback::PlaybackPosition,
14
14
-
live_audio::{LiveAudio, ToWorkerMsg},
12
12
+
live_audio::{LiveAudio, LiveAudioStatus, ToWorkerMsg},
15
13
project::song::{Song, SongOperation, ValidOperation},
16
14
};
17
15
···
41
39
#[cfg(feature = "async")]
42
40
async fn async_spin(mut f: impl FnMut() -> bool, time: Duration) {
43
41
let backoff = crossbeam_utils::Backoff::new();
44
44
-
loop {
45
45
-
if f() {
46
46
-
return;
47
47
-
}
42
42
+
loop {
43
43
+
if f() {
44
44
+
return;
45
45
+
}
48
46
49
49
-
if backoff.is_completed() {
50
50
-
async_io::Timer::after(time).await;
51
51
-
} else {
52
52
-
backoff.snooze();
53
53
-
}
47
47
+
if backoff.is_completed() {
48
48
+
async_io::Timer::after(time).await;
49
49
+
} else {
50
50
+
backoff.snooze();
54
51
}
52
52
+
}
55
53
}
56
54
57
55
impl ManageCollector {
···
78
76
#[cfg(feature = "async")]
79
77
ManageCollector::External(channel, _) => {
80
78
_ = channel.send_blocking(frees);
81
81
-
},
79
79
+
}
82
80
}
83
81
}
84
82
···
88
86
ManageCollector::Internal(_, num) => *num += frees,
89
87
ManageCollector::External(channel, _) => {
90
88
_ = channel.send(frees).await;
91
91
-
},
89
89
+
}
92
90
}
93
91
}
94
92
}
···
107
105
108
106
#[cfg(feature = "async")]
109
107
impl CollectGarbage {
110
110
-
fn new(collector: ManuallyDrop<Collector>, channel: async_channel::Receiver<usize>, to_be_freed: usize) -> Self {
108
108
+
fn new(
109
109
+
collector: ManuallyDrop<Collector>,
110
110
+
channel: async_channel::Receiver<usize>,
111
111
+
to_be_freed: usize,
112
112
+
) -> Self {
111
113
Self {
112
114
collector,
113
115
channel: Some(channel),
···
200
202
spin(|| self.song.try_lock().is_some(), Self::SPIN_SLEEP);
201
203
SongEdit {
202
204
song: self.song.try_lock().unwrap(),
203
203
-
gc: &mut self.gc
205
205
+
gc: &mut self.gc,
204
206
}
205
207
}
206
208
···
222
224
&mut self,
223
225
device: cpal::Device,
224
226
config: OutputConfig,
225
225
-
audio_msg_config: AudioMsgConfig,
226
226
-
msg_buffer_size: usize,
227
227
-
) -> Result<rtrb::Consumer<FromWorkerMsg>, cpal::BuildStreamError> {
227
227
+
) -> Result<triple_buffer::Output<Option<LiveAudioStatus>>, cpal::BuildStreamError> {
228
228
const TO_WORKER_CAPACITY: usize = 5;
229
229
230
230
-
let from_worker = rtrb::RingBuffer::new(msg_buffer_size);
230
230
+
let from_worker = triple_buffer::triple_buffer(&None);
231
231
let to_worker = rtrb::RingBuffer::new(TO_WORKER_CAPACITY);
232
232
let reader = self.song.build_reader().unwrap();
233
233
234
234
-
let audio_worker =
235
235
-
LiveAudio::new(reader, to_worker.1, audio_msg_config, from_worker.0, config);
234
234
+
let audio_worker = LiveAudio::new(reader, to_worker.1, from_worker.0, config);
236
235
237
236
let stream = device.build_output_stream_raw(
238
237
&config.into(),
···
309
308
async_spin(|| self.song.try_lock().is_some(), Self::SPIN_SLEEP).await;
310
309
SongEdit {
311
310
song: self.song.try_lock().unwrap(),
312
312
-
gc: &mut self.gc
311
311
+
gc: &mut self.gc,
313
312
}
314
313
}
315
314
···
331
330
332
331
/// makes the garbage collector internal again.
333
332
pub fn insert_garbage_collector(&mut self, gc: CollectGarbage) {
334
334
-
let CollectGarbage {collector, to_be_freed, channel: _} = gc;
333
333
+
let CollectGarbage {
334
334
+
collector,
335
335
+
to_be_freed,
336
336
+
channel: _,
337
337
+
} = gc;
335
338
self.gc = ManageCollector::Internal(collector, to_be_freed);
336
339
}
337
340
}
···
351
354
self.deinit_audio();
352
355
let mut song = self.edit_song();
353
356
for i in 0..Song::<true>::MAX_SAMPLES {
354
354
-
song.apply_operation(SongOperation::RemoveSample(i)).unwrap();
357
357
+
song.apply_operation(SongOperation::RemoveSample(i))
358
358
+
.unwrap();
355
359
}
356
360
song.finish();
357
361
// lock it once more to ensure that the changes were propagated
···
438
442
}),
439
443
}
440
444
}
441
441
-
}
442
442
-
443
443
-
#[derive(Default, Debug, Clone, Copy)]
444
444
-
pub struct AudioMsgConfig {
445
445
-
pub buffer_finished: bool,
446
446
-
pub playback_position: bool,
447
445
}
448
446
449
447
#[derive(Debug, Clone, Copy)]