Skip to content
This repository was archived by the owner on Dec 29, 2024. It is now read-only.

Commit 65425ec

Browse files
committed
Fix some playback issues.
1) the text now matches pro-tracker (except we print the period in hex because the note look-up is expensive) 2) samples now loop correctly 3) volume slides work correctly (I think?!)
1 parent 15449c8 commit 65425ec

File tree

3 files changed

+85
-69
lines changed

3 files changed

+85
-69
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

neoplay/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ authors = ["Jonathan 'theJPster' Pallant <[email protected]>"]
77
description = "4-channel ProTracker player for Neotro"
88

99
[dependencies]
10-
neotracker = { git = "https://github.com/thejpster/neotracker.git", version = "0.1.0" }
10+
neotracker = { git = "https://github.com/thejpster/neotracker.git", branch = "playback-fixes" }
1111
neotron-sdk = { git = "https://github.com/neotron-compute/neotron-sdk.git", branch = "file-demo", features = ["fancy-panic"] }
1212

1313
# See workspace for profile settings

neoplay/src/player.rs

Lines changed: 83 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ struct Channel {
66
volume: u8,
77
note_period: u16,
88
sample_position: neotracker::Fractional,
9-
first_pass: bool,
109
effect: Option<neotracker::Effect>,
1110
}
1211

@@ -23,6 +22,9 @@ pub struct Player<'a> {
2322
position: u8,
2423
line: u8,
2524
finished: bool,
25+
/// This is set when we get a Pattern Break (0xDxx) effect. It causes
26+
/// us to jump to a specific row in the next pattern.
27+
pattern_break: Option<u8>,
2628
channels: [Channel; 4],
2729
}
2830

@@ -46,6 +48,7 @@ impl<'a> Player<'a> {
4648
clock_ticks_per_device_sample: neotracker::Fractional::new_from_sample_rate(
4749
sample_rate,
4850
),
51+
pattern_break: None,
4952
channels: [
5053
Channel::default(),
5154
Channel::default(),
@@ -61,7 +64,17 @@ impl<'a> Player<'a> {
6164
T: core::fmt::Write,
6265
{
6366
if self.ticks_left == 0 && self.samples_left == 0 {
64-
// yes it is time for a new line
67+
// It is time for a new line
68+
69+
// Did we have a pattern break? Jump straight there.
70+
if let Some(line) = self.pattern_break {
71+
self.pattern_break = None;
72+
self.position += 1;
73+
self.line = line;
74+
}
75+
76+
// Find which line we play next. It might be the next line in this
77+
// pattern, or it might be the first line in the next pattern.
6578
let line = loop {
6679
// Work out which pattern we're playing
6780
let Some(pattern_idx) = self.modfile.song_position(self.position) else {
@@ -77,67 +90,71 @@ impl<'a> Player<'a> {
7790
self.position += 1;
7891
continue;
7992
};
93+
// There was no need to go the next pattern, so produce this
94+
// line from the loop.
8095
break line;
8196
};
8297

8398
// Load four channels with new line data
84-
let _ = write!(out, "{:03} {:06} ", self.position, self.line);
99+
let _ = write!(out, "{:03} {:06}: ", self.position, self.line);
85100
for (channel_num, ch) in self.channels.iter_mut().enumerate() {
86101
let note = &line.channel[channel_num];
102+
// Do we have a new sample to play?
87103
if note.is_empty() {
88-
let _ = write!(out, "-- ---- ----|");
104+
let _ = write!(out, "--- -----|");
89105
} else {
90-
// 0 means carry on previous note
91-
let sample = self.modfile.sample_info(note.sample_no());
92-
if let Some(sample) = sample {
93-
ch.note_period = note.period();
106+
if let Some(sample) = self.modfile.sample_info(note.sample_no()) {
94107
if note.period() != 0 {
95-
ch.volume = sample.volume();
96-
ch.sample_num = note.sample_no();
97-
ch.sample_position = neotracker::Fractional::default();
98-
ch.first_pass = true;
108+
ch.note_period = note.period();
99109
}
110+
ch.volume = sample.volume();
111+
ch.sample_num = note.sample_no();
112+
ch.sample_position = neotracker::Fractional::default();
100113
}
101114
let _ = write!(
102115
out,
103-
"{:02} {:04x} {:04x}|",
104-
note.sample_no(),
116+
"{:3x} {:02}{:03x}|",
105117
note.period(),
118+
note.sample_no(),
106119
note.effect_u16()
107120
);
108-
ch.effect = None;
109-
match note.effect() {
110-
e @ Some(
111-
neotracker::Effect::Arpeggio(_)
112-
| neotracker::Effect::SlideUp(_)
113-
| neotracker::Effect::SlideDown(_)
114-
| neotracker::Effect::VolumeSlide(_),
115-
) => {
116-
// we'll need this for later
117-
ch.effect = e;
118-
}
119-
Some(neotracker::Effect::SetVolume(value)) => {
120-
ch.volume = value;
121-
}
122-
Some(neotracker::Effect::SetSpeed(value)) => {
123-
if value <= 31 {
124-
self.ticks_per_line = u32::from(value);
125-
self.third_ticks_per_line = u32::from(value / 3);
126-
} else {
127-
// They are trying to set speed in beats per minute
128-
}
129-
}
130-
Some(neotracker::Effect::SampleOffset(n)) => {
131-
let offset = u32::from(n) * 256;
132-
ch.sample_position = neotracker::Fractional::new(offset);
133-
}
134-
Some(e) => {
135-
// eprintln!("Unhandled effect {:02x?}", e);
136-
}
137-
None => {
138-
// Do nothing
121+
}
122+
ch.effect = None;
123+
match note.effect() {
124+
e @ Some(
125+
neotracker::Effect::Arpeggio(_)
126+
| neotracker::Effect::SlideUp(_)
127+
| neotracker::Effect::SlideDown(_)
128+
| neotracker::Effect::VolumeSlide(_),
129+
) => {
130+
// we'll need this for later
131+
ch.effect = e;
132+
}
133+
Some(neotracker::Effect::SetVolume(value)) => {
134+
ch.volume = value;
135+
}
136+
Some(neotracker::Effect::SetSpeed(value)) => {
137+
if value <= 31 {
138+
self.ticks_per_line = u32::from(value);
139+
self.third_ticks_per_line = u32::from(value / 3);
140+
} else {
141+
// They are trying to set speed in beats per minute
139142
}
140143
}
144+
Some(neotracker::Effect::SampleOffset(n)) => {
145+
let offset = u32::from(n) * 256;
146+
ch.sample_position = neotracker::Fractional::new(offset);
147+
}
148+
Some(neotracker::Effect::PatternBreak(row)) => {
149+
// Start the next pattern early, at the given row
150+
self.pattern_break = Some(row);
151+
}
152+
Some(_e) => {
153+
// eprintln!("Unhandled effect {:02x?}", e);
154+
}
155+
None => {
156+
// Do nothing
157+
}
141158
}
142159
}
143160
let _ = writeln!(out);
@@ -179,12 +196,9 @@ impl<'a> Player<'a> {
179196
ch.note_period += u16::from(n);
180197
}
181198
Some(neotracker::Effect::VolumeSlide(n)) => {
182-
let xxxx = n >> 4;
183-
let yyyy = n & 0x0F;
184-
if xxxx != 0 {
185-
ch.volume = (ch.volume + xxxx).min(63);
186-
} else if yyyy != 0 {
187-
ch.volume = ch.volume.saturating_sub(yyyy);
199+
let new_volume = (ch.volume as i8) + n;
200+
if (0..=63).contains(&new_volume) {
201+
ch.volume = new_volume as u8;
188202
}
189203
}
190204
_ => {
@@ -201,35 +215,36 @@ impl<'a> Player<'a> {
201215
let mut left_sample = 0;
202216
let mut right_sample = 0;
203217
for (ch_idx, ch) in self.channels.iter_mut().enumerate() {
204-
if ch.note_period == 0 {
218+
if ch.sample_num == 0 || ch.note_period == 0 {
205219
continue;
206220
}
207221
let current_sample = self.modfile.sample(ch.sample_num).expect("bad sample");
208222
let sample_data = current_sample.raw_sample_bytes();
209-
if sample_data.len() == 0 {
223+
if sample_data.is_empty() {
210224
continue;
211225
}
212226
let integer_pos = ch.sample_position.as_index();
213-
let sample_byte = sample_data[integer_pos];
214-
let mut channel_value = sample_byte as i8 as i32;
215-
// max channel vol (64), sample range [ -128,127] scaled to [-32768, 32767]
227+
let sample_byte = sample_data.get(integer_pos).cloned().unwrap_or_default();
228+
let mut channel_value = (sample_byte as i8) as i32;
229+
// max channel vol (64), sample range [-128,127] scaled to [-32768, 32767]
216230
channel_value *= 256;
217231
channel_value *= i32::from(ch.volume);
218232
channel_value /= 64;
233+
// move the sample index by a non-integer amount
219234
ch.sample_position += self
220235
.clock_ticks_per_device_sample
221236
.apply_period(ch.note_period);
222-
223-
let new_integer_pos = ch.sample_position.as_index();
224-
let limit = if ch.first_pass {
225-
current_sample.sample_length_bytes()
226-
} else {
227-
current_sample.repeat_length_bytes()
228-
};
229-
if new_integer_pos >= limit {
230-
ch.sample_position =
231-
neotracker::Fractional::new(current_sample.repeat_point_bytes() as u32);
232-
ch.first_pass = false;
237+
// loop sample if required
238+
if current_sample.loops() {
239+
if ch.sample_position.as_index()
240+
>= (current_sample.repeat_point_bytes() + current_sample.repeat_length_bytes())
241+
{
242+
ch.sample_position =
243+
neotracker::Fractional::new(current_sample.repeat_point_bytes() as u32);
244+
}
245+
} else if ch.sample_position.as_index() >= current_sample.sample_length_bytes() {
246+
// stop playing sample
247+
ch.note_period = 0;
233248
}
234249

235250
if ch_idx == 0 || ch_idx == 3 {
@@ -238,6 +253,7 @@ impl<'a> Player<'a> {
238253
right_sample += channel_value;
239254
}
240255
}
256+
241257
(
242258
left_sample.clamp(-32768, 32767) as i16,
243259
right_sample.clamp(-32768, 32767) as i16,

0 commit comments

Comments
 (0)