mos_hardware/sid.rs
1// copyright 2022 mikael lund aka wombat
2//
3// licensed under the apache license, version 2.0 (the "license");
4// you may not use this file except in compliance with the license.
5// you may obtain a copy of the license at
6//
7// http://www.apache.org/licenses/license-2.0
8//
9// unless required by applicable law or agreed to in writing, software
10// distributed under the license is distributed on an "as is" basis,
11// without warranties or conditions of any kind, either express or implied.
12// see the license for the specific language governing permissions and
13// limitations under the license.
14
15//! Registers for the MOS Technology 6581/8580 SID (Sound Interface Device)
16//!
17//! SID is the built-in programmable sound generator chip of Commodore's CBM-II,
18//! Commodore 64, Commodore 128 and Commodore MAX Machine home computers.
19//! It was one of the first sound chips of its kind to be included in a home computer.
20
21use crate::*;
22use bitflags::bitflags;
23use core::mem::size_of;
24use rand_core::{Error, RngCore};
25use static_assertions::const_assert;
26use volatile_register::{RO, WO};
27
28bitflags! {
29 /// Control flags for the `Voice::control` register
30 pub struct VoiceControlFlags: u8 {
31 const GATE = 0b0000_0001; // bit 0
32 /// Synch fundamental frequency of oscillator with fundamental frequency of neighboring voice
33 const SYNC = 0b0000_0010; // bit 1
34 /// Set to replace triangle waveform w. ring modulation from neighbor voices
35 const RING_MODULATION = 0b0000_0100; // bit 2
36 /// Set to disable oscillations
37 const TEST = 0b0000_1000; // bit 3
38 const TRIANGLE = 0b0001_0000; // bit 4
39 const SAWTOOTH = 0b0010_0000; // bit 5
40 const PULSE = 0b0100_0000; // bit 6
41 const NOISE = 0b1000_0000; // bit 7
42 }
43}
44
45#[repr(C, packed)]
46/// Registers for a single SID voice/channel
47pub struct Voice {
48 /// `FRELO`/`FRELO` Frequency control (0x00-0x01)
49 pub frequency: WO<u16>,
50 /// `PWLO`/`PWHI` Pulse waveform width (0x02-0x03)
51 pub pulse_width: WO<u16>,
52 /// `VCREG` Control register (0x04)
53 pub control: WO<VoiceControlFlags>,
54 /// `ATDCY` Attack/decay cycle duration (0x05)
55 pub attack_decay: WO<u8>,
56 /// `SUREL` Sustain/Release Control (0x06)
57 pub sustain_release: WO<u8>,
58}
59
60/// Attack times for `ATDCY`, bits 4-7 (milliseconds, `Ms`)
61pub enum AttackTime {
62 Ms2 = 0,
63 Ms8 = 1,
64 Ms16 = 2,
65 Ms24 = 3,
66 Ms38 = 4,
67 Ms56 = 5,
68 Ms68 = 6,
69 Ms80 = 7,
70 Ms100 = 8,
71 Ms250 = 9,
72 Ms500 = 10,
73 Ms800 = 11,
74 Ms1000 = 12,
75 Ms3000 = 13,
76 Ms5000 = 14,
77 Ms8000 = 15,
78}
79
80/// Sustain times for `ATDCY`, bits 0-3 (milliseconds, `Ms`)
81pub enum DecayTime {
82 Ms6 = 0,
83 Ms24 = 1,
84 Ms48 = 2,
85 Ms72 = 3,
86 Ms114 = 4,
87 Ms168 = 5,
88 Ms204 = 6,
89 Ms240 = 7,
90 Ms300 = 8,
91 Ms750 = 9,
92 Ms1500 = 10,
93 Ms2400 = 11,
94 Ms3000 = 12,
95 Ms9000 = 13,
96 Ms15000 = 14,
97 Ms24000 = 15,
98}
99
100/// Combines attack and decay times for register `ATDCY`
101///
102/// ## Example:
103/// ~~~
104/// const TIME: u8 = combine_attack_decay(AttackTime::Ms38, DecayTime::Ms240);
105/// ~~~
106pub const fn combine_attack_decay(attack_time: AttackTime, decay_time: DecayTime) -> u8 {
107 (attack_time as u8 * 16) + (decay_time as u8)
108}
109
110impl Voice {
111 /// Sets the attack/decay cycle duration (`ATDCY`)
112 ///
113 /// See e.g. Mapping the C64, page 162.
114 ///
115 /// ## Example:
116 /// ~~~
117 /// (*c64::SID).channel1.set_attack_decay(AttackTime::Ms38, DecayTime::Ms240);
118 /// ~~~
119 pub fn set_attack_decay(&self, attack_time: AttackTime, decay_time: DecayTime) {
120 let value = combine_attack_decay(attack_time, decay_time);
121 unsafe {
122 self.attack_decay.write(value);
123 }
124 }
125}
126
127const_assert!(size_of::<Voice>() == 7);
128
129#[repr(C, packed)]
130/// MOS Technology Sound Interface Device (SID)
131pub struct MOSSoundInterfaceDevice {
132 pub channel1: Voice,
133 pub channel2: Voice,
134 pub channel3: Voice,
135 pub filter_cutoff: WO<u16>, // 0x15
136 /// `RESON` Filter resonance control (0x17)
137 pub resonance_and_filter_setup: WO<u8>,
138 /// `SIGVOL` Volume and filter select (0x18)
139 pub volume_filter_mode: WO<u8>,
140 pub potentiometer_x: RO<u8>, // 0x19
141 pub potentiometer_y: RO<u8>, // 0x1a
142 pub channel3_oscillator: RO<u8>, // 0x1b
143 pub channel3_envelope: RO<u8>, // 0x1c
144}
145
146const_assert!(size_of::<MOSSoundInterfaceDevice>() == 0x1d);
147
148impl MOSSoundInterfaceDevice {
149 /// Start noise generation on SID channel 3.
150 ///
151 /// Example:
152 /// ```
153 /// c64::sid().start_random_generator();
154 /// let random_byte = c64::sid().rand8(20);
155 /// ```
156 /// More information [here](https://www.atarimagazines.com/compute/issue72/random_numbers.php).
157 pub fn start_random_generator(&self) {
158 unsafe {
159 self.channel3.frequency.write(u16::MAX);
160 self.channel3.control.write(VoiceControlFlags::NOISE);
161 }
162 }
163
164 /// Random byte in the interval (0, max_value)
165 pub fn rand8(&self, max_value: u8) -> u8 {
166 loop {
167 let r = self.channel3_oscillator.read();
168 if r <= max_value {
169 return r;
170 }
171 }
172 }
173
174 /// Random byte in the interval (0, 255)
175 ///
176 /// # Examples
177 /// ~~~
178 /// c64::sid().start_random_generator();
179 /// let value = c64::sid().random_byte();
180 /// ~~~
181 /// More information [here](https://www.atarimagazines.com/compute/issue72/random_numbers.php).
182 /// Currently there's no way to select the subsong as this requires that the
183 /// accumulator is set. Possibly this can be done wrapping function pointers to raw
184 /// assembler code.
185 pub fn random_byte(&self) -> u8 {
186 self.channel3_oscillator.read()
187 }
188
189 /// Random word in the interval (0, max_value)
190 pub fn rand16(&self, max_value: u16) -> u16 {
191 loop {
192 let r = ((self.channel3_oscillator.read() as u16) << 8)
193 | (self.channel3_oscillator.read() as u16);
194 if r <= max_value {
195 return r;
196 }
197 }
198 }
199}
200
201/// Random number generator using the SID oscillator
202///
203/// Implements the [`rand::RngCore`](https://docs.rs/rand/latest/rand/trait.RngCore.html)
204/// trait and can thus be used with Rusts `rand` crate.
205/// For single random bytes, it is likely more efficient to use `random_byte()`
206/// from the SID chip directly, as the smallest integer implemented in `RngCore` is `u32`,
207/// i.e. four random bytes.
208///
209/// ## Examples
210/// ~~~
211/// use mos_hardware::{c64, sid};
212/// use rand::seq::SliceRandom;
213/// let mut rng = sid::SIDRng::new(c64::sid());
214/// let value = [11, 23].choose(&mut rng).unwrap(); // 11 or 23
215/// ~~~
216#[derive(Clone)]
217pub struct SIDRng {
218 sid: &'static MOSSoundInterfaceDevice,
219}
220
221impl SIDRng {
222 /// Initialize and start SID oscillator
223 pub fn new(sid_address: &'static MOSSoundInterfaceDevice) -> Self {
224 sid_address.start_random_generator();
225 Self { sid: sid_address }
226 }
227}
228
229impl RngCore for SIDRng {
230 fn next_u32(&mut self) -> u32 {
231 u32::from_ne_bytes([
232 self.sid.random_byte(),
233 self.sid.random_byte(),
234 self.sid.random_byte(),
235 self.sid.random_byte(),
236 ])
237 }
238
239 fn next_u64(&mut self) -> u64 {
240 u64::from_ne_bytes([
241 self.sid.random_byte(),
242 self.sid.random_byte(),
243 self.sid.random_byte(),
244 self.sid.random_byte(),
245 self.sid.random_byte(),
246 self.sid.random_byte(),
247 self.sid.random_byte(),
248 self.sid.random_byte(),
249 ])
250 }
251
252 fn fill_bytes(&mut self, dest: &mut [u8]) {
253 dest.iter_mut()
254 .for_each(|byte| *byte = self.sid.random_byte());
255 }
256
257 fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Error> {
258 self.fill_bytes(dest);
259 Ok(())
260 }
261}
262
263/// Trait for loading and parsing a PSID file at compile time
264///
265/// The PSID file format is described
266/// [here](https://gist.github.com/cbmeeks/2b107f0a8d36fc461ebb056e94b2f4d6).
267/// Since arrays in rust cannot be larger than `isize`, songs larger than
268/// 32 kb cannot be loaded.
269///
270/// # Examples
271/// ~~~
272/// use mos_hardware::sid::SidTune;
273/// struct Music;
274/// impl SidTune for Music {
275/// const BYTES: &'static [u8] = core::include_bytes!("last_hero.sid");
276/// }
277/// let music = Music;
278/// unsafe {
279/// music.to_memory(); // copy data to found load address (danger!)
280/// }
281/// music.init(0); // call song initialisation routine
282/// music.play(); // call this at every frame
283/// ~~~
284pub trait SidTune {
285 /// Full SID file as const byte array. Typically you would set
286 /// this with `core::include_bytes!`.
287 const BYTES: &'static [u8];
288
289 /// True if data has an optional 2-byte header stating the load address (C64 style)
290 const HAS_BASIC_LOAD_ADDRESS: bool = matches!(
291 u16::from_be_bytes([Self::BYTES[0x08], Self::BYTES[0x09]]),
292 0
293 );
294
295 /// Offset where data begins, excluding any optional 2-byte load address
296 const DATA_OFFSET: usize = match Self::HAS_BASIC_LOAD_ADDRESS {
297 true => u16::from_be_bytes([Self::BYTES[0x06], Self::BYTES[0x07]]) as usize + 2,
298 false => u16::from_be_bytes([Self::BYTES[0x06], Self::BYTES[0x07]]) as usize,
299 };
300
301 /// Length of data part (exludes the optional 2-byte load address)
302 const DATA_LEN: usize = Self::BYTES.len() - Self::DATA_OFFSET;
303
304 /// Address of init routine
305 const INIT_ADDRESS: u16 = u16::from_be_bytes([Self::BYTES[0x0a], Self::BYTES[0x0b]]);
306
307 /// Function pointer to init routine
308 const INIT_PTR: *const unsafe extern "C" fn() -> () =
309 &Self::INIT_ADDRESS as *const u16 as *const unsafe extern "C" fn() -> ();
310
311 /// Address of play routine
312 const PLAY_ADDRESS: u16 = u16::from_be_bytes([Self::BYTES[0x0c], Self::BYTES[0x0d]]);
313
314 /// Function pointer to play routine
315 const PLAY_PTR: *const unsafe extern "C" fn() -> () =
316 &Self::PLAY_ADDRESS as *const u16 as *const unsafe extern "C" fn() -> ();
317
318 /// Number of subsongs
319 const NUM_SONGS: usize = u16::from_be_bytes([Self::BYTES[0x0e], Self::BYTES[0x0f]]) as usize;
320
321 /// Load address found either in PSID header or in data part
322 const LOAD_ADDRESS: u16 = match Self::HAS_BASIC_LOAD_ADDRESS {
323 true => u16::from_le_bytes([
324 Self::BYTES[Self::DATA_OFFSET - 2],
325 Self::BYTES[Self::DATA_OFFSET - 1],
326 ]),
327 false => u16::from_be_bytes([Self::BYTES[0x08], Self::BYTES[0x09]]),
328 };
329
330 fn num_songs(&self) -> usize {
331 Self::NUM_SONGS
332 }
333
334 /// Call song initialisation routine
335 ///
336 /// Before calling the init routine found in the the PSID file, the
337 /// accumulator (A) is set to the `song` number. This is done by placing
338 /// 6502 wrapper code at the end of the SID file.
339 ///
340 /// ## Todo
341 ///
342 /// It would be nice to let the compiler decide where to place the
343 /// wrapper code (`address`), but so far no luck.
344 fn init(&self, song: u8) {
345 let [high, low] = Self::INIT_ADDRESS.to_be_bytes();
346 let address = Self::LOAD_ADDRESS as usize + Self::DATA_LEN;
347 let init_fn = &address as *const usize as *const unsafe extern "C" fn() -> ();
348 unsafe {
349 // 0xa9 = lda; 0x4c = jmp
350 *(address as *mut [u8; 5]) = [0xa9, song, 0x4c, low, high];
351 (*init_fn)();
352 }
353 }
354
355 /// Call song play routine
356 fn play(&self) {
357 unsafe { (*(Self::PLAY_PTR))() }
358 }
359
360 /// Copies data into memory at load address specified in PSID file.
361 ///
362 /// # Safety
363 /// Unsafe, as this will perform copy into hard-coded
364 /// memory pool that may clash with stack or allocated heap memory.
365 unsafe fn to_memory(&self)
366 where
367 [(); Self::DATA_LEN]:,
368 {
369 let dst = Self::LOAD_ADDRESS as *mut [u8; Self::DATA_LEN];
370 *dst = Self::BYTES[Self::DATA_OFFSET..Self::BYTES.len()]
371 .try_into()
372 .unwrap();
373 }
374}