add Sounds reader

png-branch
an 2019-02-17 21:17:35 -05:00
parent a8f04cc01c
commit 58cbdd1a57
6 changed files with 368 additions and 18 deletions

View File

@ -426,6 +426,9 @@ actual PCM data.
All integers here are big-endian unless specified.
All unspecified bytes must be set to `0` when written, although when reading
should not be checked as they may be garbage.
The type "`fixed`" refers to a 32-bit fixed point number with the format 15.16s
(the lower 16 bits are fractional, the upper 15 are integral, and one bit for
sign.)
@ -1079,10 +1082,32 @@ but due to padding, the real length is 34.
- `SndBeg`, `SndKey` and `SndEnd` are the sounds played at the first, key and
last frame of this sequence.
### Bitmap Header ###
Bitmap Header is 26 bytes.
Each Bitmap Header is followed by either `Height * 4` or `Width * 4` empty
bytes which must be skipped.
| Name | Type | Offset |
| ---- | ---- | ------ |
| `Width` | `u16` | `0` |
| `Height` | `u16` | `2` |
| `Pitch` | `u16` | `4` |
| `Flags` | `u16` | `6` |
| `Depth` | `u16` | `8` |
- `Width` is the number of pixels on the horizontal axis.
- `Height` is the number of pixels on the vertical axis.
- `Pitch` is either the number of pixels per row if row ordered, per column if
column ordered, or `65535` if the data is transparency RLE compressed.
- `Flags` is a Bitmap Flags bit field.
- `Depth` must always be `8`.
## Sounds ##
Sounds files start with a header followed by all of the actual sound
definitions. Simple enough.
definitions. Each sound starts with a Carbon Sound Header.
### Sounds Header ###
@ -1113,24 +1138,69 @@ Sound Definition is 64 bytes.
| Name | Type | Offset |
| ---- | ---- | ------ |
| sound_code | `u16` | `0` |
| `Behaviour` | `u16` | `2` |
| `Code` | `u16` | `0` |
| `Volume` | `u16` | `2` |
| `Flags` | `u16` | `4` |
| `Chance` | `u16` | `6` |
| `PitchLo` | `fixed` | `8` |
| `PitchHi` | `fixed` | `12` |
| permutations | `i16` | `16` |
| perms_played | `u16` | `18` |
| group_offset | `u32` | `20` |
| single_length | `u32` | `24` |
| total_length | `u32` | `28` |
| sound_offsets | `u32[5]` | `32` |
| `NumOfs` | `u16` | `16` |
| `GroupOffset` | `u32` | `20` |
| `Size` | `u32` | `24` |
| `GroupSize` | `u32` | `28` |
| `AddOffset` | `u32[5]` | `32` |
- `Behaviour` is a Sound Behaviour enumeration.
- `Code` is an Object ID referencing something (TODO.)
- `Volume` is a Sound Behaviour enumeration.
- `Flags` is a Sound Definition Flags bit field.
- `Chance` is the chance out of `65535` that the sound will not play.
- `PitchLo` is the lower random pitch bound, if `0` then it will be `1.0`.
- `PitchHi` is the high random pitch bound, if `0` then it will be `PitchLo`.
- `NumOfs` is the number of random sounds to pick from `AddOffset`.
- `GroupOffset` is the starting offset for each additive sound offset.
- `Size` is the sound of an individual sound in the group.
- `GroupSize` is the total size of all sounds in the group.
- `AddOffset` is the offset added to `GroupOffset` to get an individual sound.
While it is an array of `NumOfs` offsets, it has a fixed size in the format.
### Carbon Sound Header ###
Carbon Sound Header is 21 bytes.
The sound format is from Carbon's `SoundHeader` structures. It's used primarily
in System 7 programs as `snd` resources but in OS X it was deprecated in favor
of QuickTime. HFS still has Resource Forks but they aren't used anymore. I
don't imagine this format was ever used for anything else, except for Marathon,
which embeds it in the Sound files directly, instead of using `snd` resources
(which have a larger structure consisting of a resource header and sound
commands rather than just the header and sample data.)
| Name | Type | Offset |
| ---- | ---- | ------ |
| `Size` | `u32` | `4` |
| `SampleRate` | `u16` | `8` |
| `LoopBeg` | `u32` | `12` |
| `LoopEnd` | `u32` | `16` |
| `Magic` | `u8` | `20` |
- If `Magic` is `$00` nothing else needs to be done and raw signed 8-bit mono
PCM sample data starts at byte 22. If it is `$FF` it is followed by a Carbon
Extended Sound Header, or if it is `$FE` it is followed by a Carbon Compressed
Sound Header. The compressed sound header is not documented because it is not
actually used by Marathon.
### Carbon Extended Sound Header ###
Carbon Extended Sound Header is 42 bytes.
The extended sound header contains more useless information and even several
fields that do absolutely nothing. Wow. At least it can store 16 bit samples.
It also has an 80-bit float in it, which horrifies me greatly. There's only one
actually useful field.
| Name | Type | Offset |
| ---- | ---- | ------ |
| `SampleBits` | `u16` | `26` |
# ENUMERATIONS ################################################################

View File

@ -10,6 +10,7 @@ pub mod crc;
pub mod file;
pub mod fixed;
pub mod image;
pub mod sound;
pub mod text;
// EOF

138
src/durandal/sound.rs Normal file
View File

@ -0,0 +1,138 @@
//! Sound representation.
use crate::durandal::err::*;
use std::io;
pub fn write_wav(out: &mut impl io::Write, snd: &impl Sound) -> ResultS<()>
{
let rate = u32::from(snd.rate());
let bps = rate * 2;
let ssize = bps * snd.len() as u32;
let fsize = 36 + ssize;
out.write_all(b"RIFF")?;
out.write_all(&fsize.to_le_bytes())?;
out.write_all(b"WAVE")?;
out.write_all(b"fmt ")?;
out.write_all(&16u32.to_le_bytes())?;
out.write_all(&1u16.to_le_bytes())?; // PCM
out.write_all(&1u16.to_le_bytes())?; // mono
out.write_all(&rate.to_le_bytes())?; // rate
out.write_all(&bps.to_le_bytes())?; // bytes per second
out.write_all(&2u16.to_le_bytes())?; // block alignment
out.write_all(&16u16.to_le_bytes())?; // bits per sample
out.write_all(b"data")?;
out.write_all(&ssize.to_le_bytes())?;
for p in 0..snd.len() {
let sample = snd.index(p);
out.write_all(&sample.to_le_bytes())?;
}
if ssize & 1 == 1 {
out.write_all(&[0])?;
}
Ok(())
}
pub trait Sound
{
fn rate(&self) -> u16;
fn len(&self) -> usize;
fn index(&self, p: usize) -> i16;
fn is_empty(&self) -> bool
{
self.len() == 0
}
fn get(&self, p: usize) -> Option<i16>
{
if p < self.len() {
Some(self.index(p))
} else {
None
}
}
}
impl Sound8
{
/// Creates a new Sound8.
pub fn new(rate: u16, len: usize) -> Self
{
Self{rate, data: Vec::with_capacity(len)}
}
}
impl Sound16
{
/// Creates a new Sound16.
pub fn new(rate: u16, len: usize) -> Self
{
Self{rate, data: Vec::with_capacity(len)}
}
/// Creates a new Sound16 from an unsigned 8-bit stream.
pub fn new_from_8(rate: u16, b: &[u8]) -> Self
{
let mut snd = Sound16::new(rate, b.len());
for &sample in b {
snd.data.push(Sound16::sample_from_8(sample));
}
snd
}
/// Creates a new Sound16 from a signed 16-bit stream.
pub fn new_from_16(rate: u16, b: &[u8]) -> Self
{
let mut snd = Sound16::new(rate, b.len() / 2);
for (&x, &y) in b.iter().step_by(2).zip(b.iter().step_by(2).next()) {
snd.data.push(i16::from_be_bytes([x, y]));
}
snd
}
/// Creates a signed 16-bit sample from an unsigned 8-bit sample.
pub fn sample_from_8(sample: u8) -> i16
{
i16::from(sample) - 0x80 << 8
}
}
impl Sound for Sound8
{
fn rate(&self) -> u16 {self.rate}
fn len(&self) -> usize {self.data.len()}
fn index(&self, p: usize) -> i16
{
Sound16::sample_from_8(self.data[p])
}
}
impl Sound for Sound16
{
fn rate(&self) -> u16 {self.rate}
fn len(&self) -> usize {self.data.len()}
fn index(&self, p: usize) -> i16 {self.data[p]}
}
pub struct Sound8
{
rate: u16,
pub data: Vec<u8>,
}
pub struct Sound16
{
rate: u16,
pub data: Vec<i16>,
}
// EOF

View File

@ -1,5 +1,5 @@
use maraiah::{durandal::{bin::*, chunk::*, err::*, file::*, image::*, text::*},
marathon::{machdr, map, pict, shp, term, wad}};
use maraiah::{durandal::{bin::*, err::*, file::*, image::*, sound::*, text::*},
marathon::{machdr, map, pict, shp, snd, term, wad}};
use std::{collections::HashSet,
fs,
io::{self, Write}};
@ -31,6 +31,12 @@ fn make_yaml<T>(opt: &Options, data: &T) -> ResultS<()>
Ok(())
}
fn make_wav(fname: &str, snd: &impl Sound) -> ResultS<()>
{
let mut out = io::BufWriter::new(fs::File::create(fname)?);
write_wav(&mut out, snd)
}
fn dump_chunk(opt: &Options, cid: Ident, cnk: &[u8], eid: u16) -> ResultS<()>
{
if opt.wad_all {
@ -44,12 +50,12 @@ fn dump_chunk(opt: &Options, cid: Ident, cnk: &[u8], eid: u16) -> ResultS<()>
let im = pict::load_pict(cnk)?;
make_tga(&format!("{}/pict_{}.tga", opt.out_dir, eid), &im)?;
}
b"Minf" => make_yaml(opt, &map::Minf::chunk(cnk)?)?,
b"EPNT" => make_yaml(opt, &map::Endpoint::chunk(cnk)?)?,
b"PNTS" => make_yaml(opt, &map::Point::chunk(cnk)?)?,
b"LINS" => make_yaml(opt, &map::Line::chunk(cnk)?)?,
b"SIDS" => make_yaml(opt, &map::Side::chunk(cnk)?)?,
b"term" => make_yaml(opt, &term::Terminal::chunk(cnk)?)?,
b"Minf" => make_yaml(opt, &map::read_minf(cnk)?)?,
b"EPNT" => make_yaml(opt, &c_array::<map::Endpoint>(cnk)?)?,
b"PNTS" => make_yaml(opt, &c_array::<map::Point>(cnk)?)?,
b"LINS" => make_yaml(opt, &c_array::<map::Line>(cnk)?)?,
b"SIDS" => make_yaml(opt, &c_array::<map::Side>(cnk)?)?,
b"term" => make_yaml(opt, &term::read_term(cnk)?)?,
_ => (),
}
}
@ -129,6 +135,20 @@ fn process_shp(opt: &Options, b: &[u8]) -> ResultS<()>
Ok(())
}
fn process_snd(_opt: &Options, b: &[u8]) -> ResultS<()>
{
for (c, st) in snd::read_sounds(b)?.iter().enumerate() {
for (k, sd) in st {
for (i, snd) in sd.sounds.iter().enumerate() {
let fname = format!("out/snd{}_{}_{}.wav", c, k, i);
make_wav(&fname, snd)?;
}
}
}
Ok(())
}
fn main() -> ResultS<()>
{
use argparse::*;
@ -211,6 +231,7 @@ fn main() -> ResultS<()>
match typ {
"wad:" => process_wad(&opt, b),
"shp:" => process_shp(&opt, b),
"snd:" => process_snd(&opt, b),
_ => Err(err_msg("invalid file type specified on commandline")),
}?;
}

View File

@ -4,6 +4,7 @@ pub mod machdr;
pub mod map;
pub mod pict;
pub mod shp;
pub mod snd;
pub mod term;
pub mod wad;
pub mod xfer;

119
src/marathon/snd.rs Normal file
View File

@ -0,0 +1,119 @@
//! Marathon Sounds format handling.
use crate::durandal::{bin::*, err::*, fixed::*, sound::*};
//use bitflags::bitflags;
use serde::Serialize;
use std::collections::HashMap;
fn sound(b: &[u8]) -> ResultS<Sound16>
{
let len = c_u32b(b, 4)? as usize;
let rate = c_u16b(b, 8)?;
let loop_beg = c_u32b(b, 12)?;
let loop_end = c_u32b(b, 16)?;
let magic = c_byte(b, 20)?;
match magic {
0 => Ok(Sound16::new_from_8(rate, c_data(b, 22..22 + len)?)),
0xFF => {
let stream = c_data(b, 63..63 + len)?;
match c_u16b(b, 47)? {
16 => Ok(Sound16::new_from_16(rate, stream)),
_ => Ok(Sound16::new_from_8(rate, stream)),
}
}
_ => bail!("invalid magic number"),
}
}
fn sound_def(b: &[u8]) -> ResultS<Option<(Vec<usize>, u16, SoundDef)>>
{
let index = c_u16b(b, 0)?;
let volume = c_u16b(b, 2)?;
let flags = c_u16b(b, 4)?;
let chance = c_u16b(b, 6)?;
let pitch_lo = c_u32b(b, 8)?;
let pitch_hi = c_u32b(b, 12)?;
let n_sounds = c_u16b(b, 16)? as usize;
let grp_ofs = c_u32b(b, 20)? as usize;
let volume = Volume::from_repr(volume)?;
let pitch_lo = Fixed::from_bits(pitch_lo);
let pitch_hi = Fixed::from_bits(pitch_hi);
if index == u16::max_value() {
return Ok(None);
}
if n_sounds > 5 {
bail!("too many sounds");
}
let mut ofs = Vec::with_capacity(n_sounds);
let mut p = 36;
for _ in 0..n_sounds {
ofs.push(grp_ofs + c_u32b(b, p)? as usize);
p += 4;
}
Ok(Some((ofs, index, SoundDef{volume, chance, pitch_lo, pitch_hi,
sounds: Vec::with_capacity(n_sounds)})))
}
pub fn read_sounds(b: &[u8]) -> ResultS<Vec<SoundTable>>
{
let version = c_u32b(b, 0)?;
let magic = c_iden(b, 4)?;
let src_num = c_u16b(b, 8)? as usize; // TODO
let snd_num = c_u16b(b, 10)? as usize;
if version != 1 || magic != *b"snd2" {
bail!("bad sound header");
}
let mut sc = Vec::with_capacity(src_num);
let mut p = 260;
for _ in 0..src_num {
let mut st = HashMap::with_capacity(snd_num);
for _ in 0..snd_num {
if let Some((ofs, idx, mut def)) = sound_def(c_data(b, p..p + 64)?)? {
for &ofs in &ofs {
def.sounds.push(sound(c_data(b, ofs..)?)?);
}
st.insert(idx, def);
}
p += 64;
}
sc.push(st);
}
Ok(sc)
}
pub struct SoundDef
{
pub volume: Volume,
pub chance: u16,
pub pitch_lo: Fixed,
pub pitch_hi: Fixed,
pub sounds: Vec<Sound16>,
}
type SoundTable = HashMap<u16, SoundDef>;
c_enum! {
#[derive(Debug, Serialize)]
pub enum Volume: u16
{
0 => Quiet,
1 => Normal,
2 => Loud,
}
}
// EOF