mirror of
https://github.com/storytold/soundcraft.git
synced 2026-10-24 10:36:46 -06:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e103e2e45f | ||
|
|
353acea88c |
@@ -239,6 +239,37 @@ fn info_of(reader: &dyn FormatReader, detected: FileFormat) -> Result<(u32, Audi
|
||||
))
|
||||
}
|
||||
|
||||
/// A demux error ends the decode; it is only an error when nothing was decoded yet.
|
||||
fn demux_failure(frames: u64, e: SymError) -> Result<()> {
|
||||
if frames == 0 {
|
||||
return Err(map_err(e));
|
||||
}
|
||||
log::warn!("stopping decode after demux error: {e}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Note a corrupt packet (the `errors`-th); a stream that never decodes is malformed.
|
||||
fn corrupt_packet(errors: usize, frames: u64, msg: &str) -> Result<()> {
|
||||
log::debug!("skipping corrupt packet: {msg}");
|
||||
if errors > 1000 && frames == 0 {
|
||||
return Err(AudioError::Malformed(format!("no decodable packets: {msg}")));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The decoded stream is authoritative for the channel count: record it and allocate the planes.
|
||||
fn init_planar(info: &mut AudioInfo, ch: usize) -> Result<Vec<Vec<f32>>> {
|
||||
info.channels = u16::try_from(ch).map_err(|_| AudioError::Unsupported("too many channels".into()))?;
|
||||
Ok((0..ch).map(|_| Vec::new()).collect())
|
||||
}
|
||||
|
||||
/// Append one decoded block to the planes, replacing non-finite samples with silence.
|
||||
fn append_planar(planar: &mut [Vec<f32>], scratch: &[Vec<f32>]) {
|
||||
for (dst, src) in planar.iter_mut().zip(scratch) {
|
||||
dst.extend(src.iter().map(|&s| if s.is_finite() { s } else { 0.0 }));
|
||||
}
|
||||
}
|
||||
|
||||
/// Decode the whole first audio track. When `count_only`, samples are counted but not stored.
|
||||
fn run(bytes: &[u8], detected: FileFormat, count_only: bool) -> Result<(AudioInfo, AudioBuffer)> {
|
||||
let mut reader = match open(bytes, detected)? {
|
||||
@@ -280,10 +311,7 @@ fn run(bytes: &[u8], detected: FileFormat, count_only: bool) -> Result<(AudioInf
|
||||
Err(SymError::IoError(e)) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
|
||||
Err(SymError::ResetRequired) => break,
|
||||
Err(e) => {
|
||||
if frames == 0 {
|
||||
return Err(map_err(e));
|
||||
}
|
||||
log::warn!("stopping decode after demux error: {e}");
|
||||
demux_failure(frames, e)?;
|
||||
break;
|
||||
}
|
||||
};
|
||||
@@ -294,10 +322,7 @@ fn run(bytes: &[u8], detected: FileFormat, count_only: bool) -> Result<(AudioInf
|
||||
Ok(d) => d,
|
||||
Err(SymError::DecodeError(m)) => {
|
||||
errors += 1;
|
||||
log::debug!("skipping corrupt packet: {m}");
|
||||
if errors > 1000 && frames == 0 {
|
||||
return Err(AudioError::Malformed(format!("no decodable packets: {m}")));
|
||||
}
|
||||
corrupt_packet(errors, frames, m)?;
|
||||
continue;
|
||||
}
|
||||
Err(SymError::IoError(_)) => break,
|
||||
@@ -309,9 +334,7 @@ fn run(bytes: &[u8], detected: FileFormat, count_only: bool) -> Result<(AudioInf
|
||||
continue;
|
||||
}
|
||||
if planar.is_empty() {
|
||||
// The decoded stream is authoritative for the channel count.
|
||||
info.channels = u16::try_from(ch).map_err(|_| AudioError::Unsupported("too many channels".into()))?;
|
||||
planar = (0..ch).map(|_| Vec::new()).collect();
|
||||
planar = init_planar(&mut info, ch)?;
|
||||
}
|
||||
let n = decoded.frames() as u64;
|
||||
frames = frames.saturating_add(n);
|
||||
@@ -322,9 +345,7 @@ fn run(bytes: &[u8], detected: FileFormat, count_only: bool) -> Result<(AudioInf
|
||||
continue;
|
||||
}
|
||||
decoded.copy_to_vecs_planar::<f32>(&mut scratch);
|
||||
for (dst, src) in planar.iter_mut().zip(&scratch) {
|
||||
dst.extend(src.iter().map(|&s| if s.is_finite() { s } else { 0.0 }));
|
||||
}
|
||||
append_planar(&mut planar, &scratch);
|
||||
}
|
||||
if info.channels == 0 {
|
||||
return Err(AudioError::Malformed("stream contains no audio".into()));
|
||||
|
||||
+41
-21
@@ -164,6 +164,45 @@ fn only_chunks_follow(b: &[u8], from: usize) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// A `data` chunk header as found in the file, for [`data_chunk_len`].
|
||||
struct DataChunk {
|
||||
body: usize,
|
||||
size32: u32,
|
||||
is_rf64: bool,
|
||||
remaining: usize,
|
||||
declared: usize,
|
||||
}
|
||||
|
||||
/// Bytes of audio a `data` chunk holds: leniently, whatever is present when the size is wrong or
|
||||
/// the recording was never finalised.
|
||||
fn data_chunk_len(b: &[u8], c: DataChunk) -> usize {
|
||||
let finished = u64::from(le_u32(b, 4).unwrap_or(0)) + 8 == b.len() as u64;
|
||||
let empty = c.size32 == 0 && !c.is_rf64 && finished && only_chunks_follow(b, c.body);
|
||||
if c.size32 == 0 && !c.is_rf64 && !empty { c.remaining } else { c.declared.min(c.remaining) }
|
||||
}
|
||||
|
||||
/// The sample layout of a `fmt` chunk, `None` for a format we do not read natively.
|
||||
fn pcm_layout(fmt: &Fmt) -> Result<Option<(PcmKind, SampleFormat)>> {
|
||||
Ok(Some(match (fmt.tag, fmt.bits) {
|
||||
(WAVE_FORMAT_PCM, bits @ 1..=32) => {
|
||||
let bytes = usize::from(bits.div_ceil(8));
|
||||
let sf = match fmt.valid_bits.min(bits) {
|
||||
0..=8 => SampleFormat::Int8,
|
||||
9..=16 => SampleFormat::Int16,
|
||||
17..=24 => SampleFormat::Int24,
|
||||
_ => SampleFormat::Int32,
|
||||
};
|
||||
(PcmKind::Int { bytes, big_endian: false, unsigned: bytes == 1 }, sf)
|
||||
}
|
||||
(WAVE_FORMAT_IEEE_FLOAT, 32) => (PcmKind::Float { bytes: 4, big_endian: false }, SampleFormat::Float32),
|
||||
(WAVE_FORMAT_IEEE_FLOAT, 64) => (PcmKind::Float { bytes: 8, big_endian: false }, SampleFormat::Float64),
|
||||
(WAVE_FORMAT_PCM | WAVE_FORMAT_IEEE_FLOAT, bits) => {
|
||||
return Err(AudioError::Unsupported(format!("WAV format {} with {bits} bits per sample", fmt.tag)));
|
||||
}
|
||||
_ => return Ok(None),
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) fn parse(b: &[u8]) -> Result<WavParse> {
|
||||
if !is_wav(b) {
|
||||
return Err(AudioError::Malformed("not a RIFF/RF64 WAVE file".into()));
|
||||
@@ -195,9 +234,7 @@ pub(crate) fn parse(b: &[u8]) -> Result<WavParse> {
|
||||
// A size of 0 in a file whose RIFF size is right and that has only chunks after it is an
|
||||
// empty data chunk followed by metadata (LIST, bext, …); otherwise it is a recording
|
||||
// that was never finalised, and what follows is audio.
|
||||
let finished = u64::from(le_u32(b, 4).unwrap_or(0)) + 8 == b.len() as u64;
|
||||
let empty = size32 == 0 && !is_rf64 && finished && only_chunks_follow(b, body);
|
||||
let len = if size32 == 0 && !is_rf64 && !empty { remaining } else { declared.min(remaining) };
|
||||
let len = data_chunk_len(b, DataChunk { body, size32, is_rf64, remaining, declared });
|
||||
if data.is_none() {
|
||||
data = Some((body, len));
|
||||
}
|
||||
@@ -232,24 +269,7 @@ pub(crate) fn parse(b: &[u8]) -> Result<WavParse> {
|
||||
return Err(AudioError::Malformed("WAV declares a zero sample rate".into()));
|
||||
}
|
||||
let channels = usize::from(fmt.channels);
|
||||
let (kind, sample_format) = match (fmt.tag, fmt.bits) {
|
||||
(WAVE_FORMAT_PCM, bits @ 1..=32) => {
|
||||
let bytes = usize::from(bits.div_ceil(8));
|
||||
let sf = match fmt.valid_bits.min(bits) {
|
||||
0..=8 => SampleFormat::Int8,
|
||||
9..=16 => SampleFormat::Int16,
|
||||
17..=24 => SampleFormat::Int24,
|
||||
_ => SampleFormat::Int32,
|
||||
};
|
||||
(PcmKind::Int { bytes, big_endian: false, unsigned: bytes == 1 }, sf)
|
||||
}
|
||||
(WAVE_FORMAT_IEEE_FLOAT, 32) => (PcmKind::Float { bytes: 4, big_endian: false }, SampleFormat::Float32),
|
||||
(WAVE_FORMAT_IEEE_FLOAT, 64) => (PcmKind::Float { bytes: 8, big_endian: false }, SampleFormat::Float64),
|
||||
(WAVE_FORMAT_PCM | WAVE_FORMAT_IEEE_FLOAT, bits) => {
|
||||
return Err(AudioError::Unsupported(format!("WAV format {} with {bits} bits per sample", fmt.tag)));
|
||||
}
|
||||
_ => return Ok(WavParse::Foreign { bwf }),
|
||||
};
|
||||
let Some((kind, sample_format)) = pcm_layout(&fmt)? else { return Ok(WavParse::Foreign { bwf }) };
|
||||
let block = kind.bytes().checked_mul(channels).ok_or_else(|| AudioError::Malformed("block size overflow".into()))?;
|
||||
if usize::from(fmt.block_align) != block {
|
||||
log::debug!("WAV block_align {} disagrees with computed {block}; using computed", fmt.block_align);
|
||||
|
||||
@@ -102,6 +102,35 @@ fn note_mode(p: Option<NotePort>) -> NoteMode {
|
||||
}
|
||||
}
|
||||
|
||||
/// Copy `n` frames from `start` of the host buffers into the plugin's input `port` (mono sources
|
||||
/// fill every port channel).
|
||||
fn feed_input(inputs: &mut PortBuffers, port: usize, io: &[Vec<f32>], ch: usize, (start, n): (usize, usize)) {
|
||||
for c in 0..inputs.channels(port) {
|
||||
let src = io.get(c.min(ch - 1)).and_then(|s| s.get(start..start + n));
|
||||
if let (Some(src), Some(dst)) = (src, inputs.channel_mut(port, c).and_then(|d| d.get_mut(..n))) {
|
||||
dst.copy_from_slice(src);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Copy `n` frames of the plugin's output `port` into the host buffers from `start`; a mono host
|
||||
/// buffer takes the average of a stereo port.
|
||||
fn read_output(outputs: &PortBuffers, port: usize, io: &mut [Vec<f32>], ch: usize, (start, n): (usize, usize)) {
|
||||
let oc = outputs.channels(port);
|
||||
for (c, dst) in io.iter_mut().take(ch).enumerate() {
|
||||
let Some(dst) = dst.get_mut(start..start + n) else { continue };
|
||||
if ch == 1 && oc >= 2 {
|
||||
if let (Some(l), Some(r)) = (outputs.channel(port, 0), outputs.channel(port, 1)) {
|
||||
for ((d, a), b) in dst.iter_mut().zip(l).zip(r) {
|
||||
*d = 0.5 * (a + b);
|
||||
}
|
||||
}
|
||||
} else if let Some(src) = outputs.channel(port, c.min(oc.saturating_sub(1))).and_then(|s| s.get(..n)) {
|
||||
dst.copy_from_slice(src);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Plugin for ClapPlugin {
|
||||
fn info(&self) -> &'static PluginInfo {
|
||||
self.info
|
||||
@@ -197,12 +226,7 @@ impl Plugin for ClapPlugin {
|
||||
let end = u32::try_from(start + n).unwrap_or(u32::MAX);
|
||||
// Feed the main input port (mono sources fill every port channel).
|
||||
if let Some(p) = self.in_main {
|
||||
for c in 0..self.inputs.channels(p) {
|
||||
let src = io.get(c.min(ch - 1)).and_then(|s| s.get(start..start + n));
|
||||
if let (Some(src), Some(dst)) = (src, self.inputs.channel_mut(p, c).and_then(|d| d.get_mut(..n))) {
|
||||
dst.copy_from_slice(src);
|
||||
}
|
||||
}
|
||||
feed_input(&mut self.inputs, p, io, ch, (start, n));
|
||||
}
|
||||
let k = self.queue.iter().position(|e| e.time() >= end).unwrap_or(self.queue.len());
|
||||
let offset = u32::try_from(start).unwrap_or(u32::MAX);
|
||||
@@ -212,19 +236,7 @@ impl Plugin for ClapPlugin {
|
||||
self.steady = self.steady.saturating_add(i64::try_from(n).unwrap_or(0));
|
||||
match (r, self.out_main) {
|
||||
(Ok(_), Some(p)) => {
|
||||
let oc = self.outputs.channels(p);
|
||||
for (c, dst) in io.iter_mut().take(ch).enumerate() {
|
||||
let Some(dst) = dst.get_mut(start..start + n) else { continue };
|
||||
if ch == 1 && oc >= 2 {
|
||||
if let (Some(l), Some(r)) = (self.outputs.channel(p, 0), self.outputs.channel(p, 1)) {
|
||||
for ((d, a), b) in dst.iter_mut().zip(l).zip(r) {
|
||||
*d = 0.5 * (a + b);
|
||||
}
|
||||
}
|
||||
} else if let Some(src) = self.outputs.channel(p, c.min(oc.saturating_sub(1))).and_then(|s| s.get(..n)) {
|
||||
dst.copy_from_slice(src);
|
||||
}
|
||||
}
|
||||
read_output(&self.outputs, p, io, ch, (start, n));
|
||||
}
|
||||
(Err(e), _) => log::debug!("{}: {e}", self.info.id),
|
||||
_ => {}
|
||||
|
||||
@@ -189,6 +189,61 @@ fn apply(f: &mut Fx, h: &clap_event_header) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Fill every output channel with 1.0 when all ports have the wide layout, else -1.0.
|
||||
unsafe fn fill_wide(pr: &clap_process, ports: u32, channels: u32, n: usize) {
|
||||
let fits = |bufs: *const clap_audio_buffer, n: u32| {
|
||||
n == ports && (0..n as usize).all(|i| unsafe { &*bufs.add(i) }.channel_count == channels)
|
||||
};
|
||||
let ok = fits(pr.audio_inputs, pr.audio_inputs_count) && fits(pr.audio_outputs, pr.audio_outputs_count);
|
||||
if pr.audio_outputs_count > 0 {
|
||||
let out = unsafe { &*pr.audio_outputs };
|
||||
for c in 0..out.channel_count as usize {
|
||||
let o = unsafe { *out.data32.add(c) };
|
||||
for i in 0..n {
|
||||
unsafe { *o.add(i) = if ok { 1.0 } else { -1.0 } };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Push the host-visible parameter edit and an ignorable note event onto the output queue.
|
||||
unsafe fn push_gui_edit(pr: &clap_process) {
|
||||
let e = clap_event_param_value {
|
||||
header: clap_event_header {
|
||||
size: std::mem::size_of::<clap_event_param_value>() as u32,
|
||||
time: 0,
|
||||
space_id: CLAP_CORE_EVENT_SPACE_ID,
|
||||
type_: CLAP_EVENT_PARAM_VALUE,
|
||||
flags: 0,
|
||||
},
|
||||
param_id: 7,
|
||||
cookie: std::ptr::null_mut(),
|
||||
note_id: -1,
|
||||
port_index: -1,
|
||||
channel: -1,
|
||||
key: -1,
|
||||
value: 0.25,
|
||||
};
|
||||
let out = unsafe { &*pr.out_events };
|
||||
assert!(unsafe { (out.try_push.unwrap())(pr.out_events, &e.header) });
|
||||
// Other event types are accepted and ignored by the host.
|
||||
let note = clap_event_note {
|
||||
header: clap_event_header {
|
||||
size: std::mem::size_of::<clap_event_note>() as u32,
|
||||
time: 0,
|
||||
space_id: CLAP_CORE_EVENT_SPACE_ID,
|
||||
type_: CLAP_EVENT_NOTE_END,
|
||||
flags: 0,
|
||||
},
|
||||
note_id: -1,
|
||||
port_index: 0,
|
||||
channel: 0,
|
||||
key: 60,
|
||||
velocity: 0.0,
|
||||
};
|
||||
assert!(unsafe { (out.try_push.unwrap())(pr.out_events, ¬e.header) });
|
||||
}
|
||||
|
||||
unsafe extern "C" fn process(p: *const clap_plugin, pr: *const clap_process) -> clap_process_status {
|
||||
let f = unsafe { fx(p) };
|
||||
let pr = unsafe { &*pr };
|
||||
@@ -197,19 +252,7 @@ unsafe extern "C" fn process(p: *const clap_plugin, pr: *const clap_process) ->
|
||||
let ev = unsafe { &*pr.in_events };
|
||||
let count = unsafe { (ev.size.unwrap())(pr.in_events) };
|
||||
if let Some((ports, channels)) = f.wide {
|
||||
let fits = |bufs: *const clap_audio_buffer, n: u32| {
|
||||
n == ports && (0..n as usize).all(|i| unsafe { &*bufs.add(i) }.channel_count == channels)
|
||||
};
|
||||
let ok = fits(pr.audio_inputs, pr.audio_inputs_count) && fits(pr.audio_outputs, pr.audio_outputs_count);
|
||||
if pr.audio_outputs_count > 0 {
|
||||
let out = unsafe { &*pr.audio_outputs };
|
||||
for c in 0..out.channel_count as usize {
|
||||
let o = unsafe { *out.data32.add(c) };
|
||||
for i in 0..n {
|
||||
unsafe { *o.add(i) = if ok { 1.0 } else { -1.0 } };
|
||||
}
|
||||
}
|
||||
}
|
||||
unsafe { fill_wide(pr, ports, channels, n) };
|
||||
return CLAP_PROCESS_CONTINUE;
|
||||
}
|
||||
if pr.audio_outputs_count != 1 {
|
||||
@@ -230,40 +273,7 @@ unsafe extern "C" fn process(p: *const clap_plugin, pr: *const clap_process) ->
|
||||
if f.gui_edit_pending {
|
||||
f.gui_edit_pending = false;
|
||||
f.gain = 0.25;
|
||||
let e = clap_event_param_value {
|
||||
header: clap_event_header {
|
||||
size: std::mem::size_of::<clap_event_param_value>() as u32,
|
||||
time: 0,
|
||||
space_id: CLAP_CORE_EVENT_SPACE_ID,
|
||||
type_: CLAP_EVENT_PARAM_VALUE,
|
||||
flags: 0,
|
||||
},
|
||||
param_id: 7,
|
||||
cookie: std::ptr::null_mut(),
|
||||
note_id: -1,
|
||||
port_index: -1,
|
||||
channel: -1,
|
||||
key: -1,
|
||||
value: 0.25,
|
||||
};
|
||||
let out = unsafe { &*pr.out_events };
|
||||
assert!(unsafe { (out.try_push.unwrap())(pr.out_events, &e.header) });
|
||||
// Other event types are accepted and ignored by the host.
|
||||
let note = clap_event_note {
|
||||
header: clap_event_header {
|
||||
size: std::mem::size_of::<clap_event_note>() as u32,
|
||||
time: 0,
|
||||
space_id: CLAP_CORE_EVENT_SPACE_ID,
|
||||
type_: CLAP_EVENT_NOTE_END,
|
||||
flags: 0,
|
||||
},
|
||||
note_id: -1,
|
||||
port_index: 0,
|
||||
channel: 0,
|
||||
key: 60,
|
||||
velocity: 0.0,
|
||||
};
|
||||
assert!(unsafe { (out.try_push.unwrap())(pr.out_events, ¬e.header) });
|
||||
unsafe { push_gui_edit(pr) };
|
||||
}
|
||||
let mut next = 0u32;
|
||||
let mut last_time = 0u32;
|
||||
|
||||
+74
-63
@@ -243,6 +243,77 @@ pub fn resample(ch: &[Vec<f32>], from: u32, to: u32) -> Vec<Vec<f32>> {
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Time stretch (WSOLA) and pitch shift
|
||||
|
||||
/// Start position in the guide signal whose `hop` samples best match the natural continuation at
|
||||
/// `natural`, searched within `tol` of `nominal` (coarse stride 4, then refined +-3).
|
||||
fn best_start(g: &impl Fn(i64) -> f32, nominal: i64, natural: i64, tol: i64, hop: usize) -> i64 {
|
||||
// Similarity of candidate start `p` with the natural continuation (stride 2).
|
||||
let score = |p: i64| -> f32 {
|
||||
let (mut xy, mut yy) = (0.0f32, 1e-9f32);
|
||||
let mut i = 0i64;
|
||||
while i < hop as i64 {
|
||||
let a = g(natural + i);
|
||||
let b = g(p + i);
|
||||
xy += a * b;
|
||||
yy += b * b;
|
||||
i += 2;
|
||||
}
|
||||
xy / yy.sqrt()
|
||||
};
|
||||
let lo = (nominal - tol).max(0);
|
||||
let hi = nominal + tol;
|
||||
let mut best = nominal.max(0);
|
||||
let mut best_s = f32::NEG_INFINITY;
|
||||
let mut p = lo;
|
||||
while p <= hi {
|
||||
let s = score(p);
|
||||
if s > best_s {
|
||||
best_s = s;
|
||||
best = p;
|
||||
}
|
||||
p += 4;
|
||||
}
|
||||
let (rlo, rhi) = ((best - 3).max(lo), (best + 3).min(hi));
|
||||
for p in rlo..=rhi {
|
||||
let s = score(p);
|
||||
if s > best_s {
|
||||
best_s = s;
|
||||
best = p;
|
||||
}
|
||||
}
|
||||
best
|
||||
}
|
||||
|
||||
/// Add one windowed frame of every channel, read from `at.0`, into `out` at `at.1`, and the window
|
||||
/// into `wsum`.
|
||||
fn overlap_add(ch: &[Vec<f32>], out: &mut [Vec<f32>], wsum: &mut [f32], window: &[f32], at: (usize, usize)) {
|
||||
let (in_pos, out_pos) = at;
|
||||
for (c, o) in ch.iter().zip(out.iter_mut()) {
|
||||
for (i, w) in window.iter().enumerate() {
|
||||
let v = c.get(in_pos + i).copied().unwrap_or(0.0);
|
||||
if let Some(d) = o.get_mut(out_pos + i) {
|
||||
*d += v * w;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (i, w) in window.iter().enumerate() {
|
||||
if let Some(d) = wsum.get_mut(out_pos + i) {
|
||||
*d += w;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Divide out the accumulated window weights and cut each channel to `out_len`.
|
||||
fn normalise(out: &mut [Vec<f32>], wsum: &[f32], out_len: usize) {
|
||||
for o in out.iter_mut() {
|
||||
for (s, w) in o.iter_mut().zip(wsum) {
|
||||
if *w > 1e-3 {
|
||||
*s /= w;
|
||||
}
|
||||
}
|
||||
o.truncate(out_len);
|
||||
}
|
||||
}
|
||||
|
||||
/// Pitch-preserving time stretch by WSOLA (waveform-similarity overlap-add).
|
||||
/// `ratio` = output length / input length, clamped to 0.1 ..= 10. All channels share the same
|
||||
/// segment choices so the stereo image stays coherent.
|
||||
@@ -275,72 +346,12 @@ pub fn time_stretch(ch: &[Vec<f32>], ratio: f64, sample_rate: f32) -> Vec<Vec<f3
|
||||
if out_pos >= out_len {
|
||||
break;
|
||||
}
|
||||
let in_pos = if k == 0 {
|
||||
0
|
||||
} else {
|
||||
let nominal = (out_pos as f64 / ratio).round() as i64;
|
||||
let natural = prev_in + hop as i64;
|
||||
// Similarity of candidate start `p` with the natural continuation (stride 2).
|
||||
let score = |p: i64| -> f32 {
|
||||
let (mut xy, mut yy) = (0.0f32, 1e-9f32);
|
||||
let mut i = 0i64;
|
||||
while i < hop as i64 {
|
||||
let a = g(natural + i);
|
||||
let b = g(p + i);
|
||||
xy += a * b;
|
||||
yy += b * b;
|
||||
i += 2;
|
||||
}
|
||||
xy / yy.sqrt()
|
||||
};
|
||||
let lo = (nominal - tol).max(0);
|
||||
let hi = nominal + tol;
|
||||
let mut best = nominal.max(0);
|
||||
let mut best_s = f32::NEG_INFINITY;
|
||||
let mut p = lo;
|
||||
while p <= hi {
|
||||
let s = score(p);
|
||||
if s > best_s {
|
||||
best_s = s;
|
||||
best = p;
|
||||
}
|
||||
p += 4;
|
||||
}
|
||||
let (rlo, rhi) = ((best - 3).max(lo), (best + 3).min(hi));
|
||||
for p in rlo..=rhi {
|
||||
let s = score(p);
|
||||
if s > best_s {
|
||||
best_s = s;
|
||||
best = p;
|
||||
}
|
||||
}
|
||||
best
|
||||
};
|
||||
for (c, o) in ch.iter().zip(out.iter_mut()) {
|
||||
for (i, w) in window.iter().enumerate() {
|
||||
let src = in_pos as usize + i;
|
||||
let v = c.get(src).copied().unwrap_or(0.0);
|
||||
if let Some(d) = o.get_mut(out_pos + i) {
|
||||
*d += v * w;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (i, w) in window.iter().enumerate() {
|
||||
if let Some(d) = wsum.get_mut(out_pos + i) {
|
||||
*d += w;
|
||||
}
|
||||
}
|
||||
let in_pos = if k == 0 { 0 } else { best_start(&g, (out_pos as f64 / ratio).round() as i64, prev_in + hop as i64, tol, hop) };
|
||||
overlap_add(ch, &mut out, &mut wsum, &window, (in_pos as usize, out_pos));
|
||||
prev_in = in_pos;
|
||||
k += 1;
|
||||
}
|
||||
for o in out.iter_mut() {
|
||||
for (s, w) in o.iter_mut().zip(&wsum) {
|
||||
if *w > 1e-3 {
|
||||
*s /= w;
|
||||
}
|
||||
}
|
||||
o.truncate(out_len);
|
||||
}
|
||||
normalise(&mut out, &wsum, out_len);
|
||||
out
|
||||
}
|
||||
|
||||
|
||||
@@ -135,6 +135,65 @@ impl Default for DrumSynth {
|
||||
}
|
||||
}
|
||||
|
||||
/// One output sample of a sounding voice of `kind`, advancing its envelopes.
|
||||
fn voice_sample(v: &mut DrumVoice, kind: Kind, sr: f32, clap_gap: usize) -> f32 {
|
||||
match kind {
|
||||
Kind::Kick | Kind::Tom(_) => {
|
||||
v.pitch_env *= v.pitch_coef;
|
||||
let f = v.f_end + (v.f_start - v.f_end) * v.pitch_env;
|
||||
v.phase += f / sr;
|
||||
v.phase -= v.phase.floor();
|
||||
let tone = (v.phase * TAU).sin() * v.amp;
|
||||
let click = v.filt.process_sample(v.rng.bipolar()) * v.noise_amp;
|
||||
v.amp *= v.amp_coef;
|
||||
v.noise_amp *= v.noise_coef;
|
||||
tone + click
|
||||
}
|
||||
Kind::Snare => {
|
||||
v.pitch_env *= v.pitch_coef;
|
||||
let f = v.f_end + (v.f_start - v.f_end) * v.pitch_env;
|
||||
v.phase += f / sr;
|
||||
v.phase -= v.phase.floor();
|
||||
let tone = (v.phase * TAU).sin() * v.amp * 0.6;
|
||||
let n = v.filt2.process_sample(v.filt.process_sample(v.rng.bipolar())) * v.noise_amp * 0.7;
|
||||
v.amp *= v.amp_coef;
|
||||
v.noise_amp *= v.noise_coef;
|
||||
tone + n
|
||||
}
|
||||
Kind::Clap => {
|
||||
// Three quick bursts then a diffuse tail.
|
||||
let burst = if v.t < clap_gap * 3 {
|
||||
if v.t.is_multiple_of(clap_gap) {
|
||||
v.amp = 1.0;
|
||||
}
|
||||
v.amp
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let env = burst.max(if v.t >= clap_gap * 2 { v.noise_amp * 0.6 } else { 0.0 });
|
||||
let y = v.filt.process_sample(v.rng.bipolar()) * env * 2.0;
|
||||
v.amp *= v.amp_coef;
|
||||
if v.t >= clap_gap * 2 {
|
||||
v.noise_amp *= v.noise_coef;
|
||||
}
|
||||
y
|
||||
}
|
||||
Kind::ClosedHat | Kind::OpenHat | Kind::Crash | Kind::Ride => {
|
||||
let scale = if matches!(kind, Kind::Crash | Kind::Ride) { 1.0 } else { 1.9 };
|
||||
let mut metal = 0.0;
|
||||
for (m, f) in v.metal.iter_mut().zip(METAL) {
|
||||
*m += f * scale / sr;
|
||||
*m -= m.floor();
|
||||
metal += if *m < 0.5 { 1.0 } else { -1.0 };
|
||||
}
|
||||
let raw = metal / 6.0 * (1.0 - v.noise_amp * 0.5) + v.rng.bipolar() * v.noise_amp;
|
||||
let y = v.filt2.process_sample(v.filt.process_sample(raw)) * v.amp;
|
||||
v.amp *= v.amp_coef;
|
||||
y
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DrumSynth {
|
||||
pub fn new() -> Self {
|
||||
let mut voices = [DrumVoice::new(1); VOICES];
|
||||
@@ -246,61 +305,7 @@ impl DrumSynth {
|
||||
let mut out = 0.0;
|
||||
for v in &mut self.voices {
|
||||
let Some(kind) = v.kind else { continue };
|
||||
let y = match kind {
|
||||
Kind::Kick | Kind::Tom(_) => {
|
||||
v.pitch_env *= v.pitch_coef;
|
||||
let f = v.f_end + (v.f_start - v.f_end) * v.pitch_env;
|
||||
v.phase += f / sr;
|
||||
v.phase -= v.phase.floor();
|
||||
let tone = (v.phase * TAU).sin() * v.amp;
|
||||
let click = v.filt.process_sample(v.rng.bipolar()) * v.noise_amp;
|
||||
v.amp *= v.amp_coef;
|
||||
v.noise_amp *= v.noise_coef;
|
||||
tone + click
|
||||
}
|
||||
Kind::Snare => {
|
||||
v.pitch_env *= v.pitch_coef;
|
||||
let f = v.f_end + (v.f_start - v.f_end) * v.pitch_env;
|
||||
v.phase += f / sr;
|
||||
v.phase -= v.phase.floor();
|
||||
let tone = (v.phase * TAU).sin() * v.amp * 0.6;
|
||||
let n = v.filt2.process_sample(v.filt.process_sample(v.rng.bipolar())) * v.noise_amp * 0.7;
|
||||
v.amp *= v.amp_coef;
|
||||
v.noise_amp *= v.noise_coef;
|
||||
tone + n
|
||||
}
|
||||
Kind::Clap => {
|
||||
// Three quick bursts then a diffuse tail.
|
||||
let burst = if v.t < clap_gap * 3 {
|
||||
if v.t % clap_gap == 0 {
|
||||
v.amp = 1.0;
|
||||
}
|
||||
v.amp
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let env = burst.max(if v.t >= clap_gap * 2 { v.noise_amp * 0.6 } else { 0.0 });
|
||||
let y = v.filt.process_sample(v.rng.bipolar()) * env * 2.0;
|
||||
v.amp *= v.amp_coef;
|
||||
if v.t >= clap_gap * 2 {
|
||||
v.noise_amp *= v.noise_coef;
|
||||
}
|
||||
y
|
||||
}
|
||||
Kind::ClosedHat | Kind::OpenHat | Kind::Crash | Kind::Ride => {
|
||||
let scale = if matches!(kind, Kind::Crash | Kind::Ride) { 1.0 } else { 1.9 };
|
||||
let mut metal = 0.0;
|
||||
for (m, f) in v.metal.iter_mut().zip(METAL) {
|
||||
*m += f * scale / sr;
|
||||
*m -= m.floor();
|
||||
metal += if *m < 0.5 { 1.0 } else { -1.0 };
|
||||
}
|
||||
let raw = metal / 6.0 * (1.0 - v.noise_amp * 0.5) + v.rng.bipolar() * v.noise_amp;
|
||||
let y = v.filt2.process_sample(v.filt.process_sample(raw)) * v.amp;
|
||||
v.amp *= v.amp_coef;
|
||||
y
|
||||
}
|
||||
};
|
||||
let y = voice_sample(v, kind, sr, clap_gap);
|
||||
v.t += 1;
|
||||
let alive = match kind {
|
||||
Kind::Clap => v.t < clap_gap * 3 || v.noise_amp > OFF,
|
||||
|
||||
@@ -201,6 +201,31 @@ impl Reverb {
|
||||
}
|
||||
}
|
||||
|
||||
/// Input sample `n` of the left and right channels (a mono input feeds both); non-finite reads as 0.
|
||||
fn input_pair(io: &[Vec<f32>], ch: usize, n: usize) -> (f32, f32) {
|
||||
let xl = io.first().and_then(|b| b.get(n)).copied().filter(|v| v.is_finite()).unwrap_or(0.0);
|
||||
let xr = if ch > 1 { io.get(1).and_then(|b| b.get(n)).copied().filter(|v| v.is_finite()).unwrap_or(0.0) } else { xl };
|
||||
(xl, xr)
|
||||
}
|
||||
|
||||
/// Blend the wet signal `[mid, left, right]` into sample `n` of the first `ch` channels at mix `m`.
|
||||
fn mix_out(io: &mut [Vec<f32>], ch: usize, n: usize, wet: [f32; 3], m: f32) {
|
||||
let [mid, wl, wr] = wet;
|
||||
for (c, b) in io.iter_mut().take(ch).enumerate() {
|
||||
let wet = if ch == 1 {
|
||||
mid
|
||||
} else if c % 2 == 0 {
|
||||
wl
|
||||
} else {
|
||||
wr
|
||||
};
|
||||
if let Some(x) = b.get_mut(n) {
|
||||
let dry = if x.is_finite() { *x } else { 0.0 };
|
||||
*x = dry * (1.0 - m) + wet * m;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Plugin for Reverb {
|
||||
fn info(&self) -> &'static PluginInfo {
|
||||
self.info
|
||||
@@ -279,8 +304,7 @@ impl Plugin for Reverb {
|
||||
let size = self.size.value();
|
||||
let base: [f32; LINES] = self.cfg.delays_ms.map(|ms| ms_to_samples(ms * size, sr));
|
||||
for n in 0..frames {
|
||||
let xl = io.first().and_then(|b| b.get(n)).copied().filter(|v| v.is_finite()).unwrap_or(0.0);
|
||||
let xr = if ch > 1 { io.get(1).and_then(|b| b.get(n)).copied().filter(|v| v.is_finite()).unwrap_or(0.0) } else { xl };
|
||||
let (xl, xr) = input_pair(io, ch, n);
|
||||
let pre = ms_to_samples(self.pre_ms.next(), sr);
|
||||
let mut ins = [xl, xr];
|
||||
for (side, x) in ins.iter_mut().enumerate() {
|
||||
@@ -319,19 +343,7 @@ impl Plugin for Reverb {
|
||||
let side = 0.5 * (wl - wr) * w;
|
||||
let (wl, wr) = (mid + side, mid - side);
|
||||
let m = self.mix.next();
|
||||
for (c, b) in io.iter_mut().take(ch).enumerate() {
|
||||
let wet = if ch == 1 {
|
||||
mid
|
||||
} else if c % 2 == 0 {
|
||||
wl
|
||||
} else {
|
||||
wr
|
||||
};
|
||||
if let Some(x) = b.get_mut(n) {
|
||||
let dry = if x.is_finite() { *x } else { 0.0 };
|
||||
*x = dry * (1.0 - m) + wet * m;
|
||||
}
|
||||
}
|
||||
mix_out(io, ch, n, [mid, wl, wr], m);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -452,6 +452,38 @@ fn select_all(e: &mut Engine, _: &Value) -> Result<Value> {
|
||||
Ok(json!({"tracks": s.edit.selected_tracks.len(), "end": end}))
|
||||
}
|
||||
|
||||
/// Adds the members of every active edit group that touches `t`, then puts `t` in track order.
|
||||
fn add_group_members(s: &Session, t: &mut Vec<TrackId>) {
|
||||
let extra: Vec<TrackId> =
|
||||
s.groups.iter().filter(|g| g.active && g.edit && g.members.iter().any(|m| t.contains(m))).flat_map(|g| g.members.clone()).collect();
|
||||
for m in extra {
|
||||
if !t.contains(&m) {
|
||||
t.push(m);
|
||||
}
|
||||
}
|
||||
let order: Vec<TrackId> = s.tracks.iter().map(|x| x.id).collect();
|
||||
t.sort_by_key(|x| order.iter().position(|o| o == x).unwrap_or(usize::MAX));
|
||||
}
|
||||
|
||||
/// Selecting clips also selects their range and tracks (Grabber click).
|
||||
fn select_clip_ids(s: &Session, ed: &mut soundcraft_model::EditState, c: Vec<ClipId>) {
|
||||
let mut range: Option<soundcraft_time::Range> = None;
|
||||
let mut trs = Vec::new();
|
||||
for id in &c {
|
||||
if let Some((tid, clip)) = s.find_clip(*id) {
|
||||
range = Some(range.map_or(clip.range(), |r| soundcraft_time::Range::new(r.start.min(clip.start), r.end.max(clip.end()))));
|
||||
if !trs.contains(&tid) {
|
||||
trs.push(tid);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(r) = range {
|
||||
ed.selection = r;
|
||||
ed.selected_tracks = trs;
|
||||
}
|
||||
ed.selected_clips = c;
|
||||
}
|
||||
|
||||
fn select(e: &mut Engine, p: &Value) -> Result<Value> {
|
||||
let tracks = if p.get("tracks").is_some() || p.get("track").is_some() { Some(tracks_param(e, "edit.select", p)?) } else { None };
|
||||
let st = position_param(e, "edit.select", p, "start")?;
|
||||
@@ -467,15 +499,7 @@ fn select(e: &mut Engine, p: &Value) -> Result<Value> {
|
||||
if let Some(mut t) = tracks {
|
||||
// Active edit groups select all their members together.
|
||||
if !exact {
|
||||
let extra: Vec<soundcraft_model::TrackId> =
|
||||
s.groups.iter().filter(|g| g.active && g.edit && g.members.iter().any(|m| t.contains(m))).flat_map(|g| g.members.clone()).collect();
|
||||
for m in extra {
|
||||
if !t.contains(&m) {
|
||||
t.push(m);
|
||||
}
|
||||
}
|
||||
let order: Vec<soundcraft_model::TrackId> = s.tracks.iter().map(|x| x.id).collect();
|
||||
t.sort_by_key(|x| order.iter().position(|o| o == x).unwrap_or(usize::MAX));
|
||||
add_group_members(s, &mut t);
|
||||
}
|
||||
ed.selected_tracks = t;
|
||||
}
|
||||
@@ -485,22 +509,7 @@ fn select(e: &mut Engine, p: &Value) -> Result<Value> {
|
||||
_ => {}
|
||||
}
|
||||
if let Some(c) = clips {
|
||||
// Selecting clips also selects their range and tracks (Grabber click).
|
||||
let mut range: Option<soundcraft_time::Range> = None;
|
||||
let mut trs = Vec::new();
|
||||
for id in &c {
|
||||
if let Some((tid, clip)) = s.find_clip(*id) {
|
||||
range = Some(range.map_or(clip.range(), |r| soundcraft_time::Range::new(r.start.min(clip.start), r.end.max(clip.end()))));
|
||||
if !trs.contains(&tid) {
|
||||
trs.push(tid);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(r) = range {
|
||||
ed.selection = r;
|
||||
ed.selected_tracks = trs;
|
||||
}
|
||||
ed.selected_clips = c;
|
||||
select_clip_ids(s, &mut ed, c);
|
||||
} else if list_changes {
|
||||
ed.selected_clips.clear();
|
||||
}
|
||||
|
||||
@@ -856,6 +856,57 @@ fn coalesce_clip_gain_to_volume(e: &mut Engine, p: &Value) -> Result<Value> {
|
||||
Ok(json!({"clips": n}))
|
||||
}
|
||||
|
||||
/// The automation parameters a trim/glide over a range touches on `tr`.
|
||||
fn trim_params(tr: &soundcraft_model::Track, pan_only: bool) -> Vec<AutoParam> {
|
||||
let mut params: Vec<AutoParam> = tr
|
||||
.automation
|
||||
.iter()
|
||||
.filter(|l| l.enabled && !matches!(l.param, AutoParam::Mute | AutoParam::SendMute(_)))
|
||||
.filter(|l| !pan_only || matches!(l.param, AutoParam::Pan(_)))
|
||||
.map(|l| l.param.clone())
|
||||
.collect();
|
||||
if pan_only {
|
||||
for i in 0..tr.mixer.pan.len().min(2) {
|
||||
let pp = AutoParam::Pan(i as u8);
|
||||
if !params.contains(&pp) {
|
||||
params.push(pp);
|
||||
}
|
||||
}
|
||||
}
|
||||
params
|
||||
}
|
||||
|
||||
/// Trim (shift the points inside `r` by the change at its end) or glide (ramp across `r`) one lane.
|
||||
fn trim_lane(tr: &mut soundcraft_model::Track, param: &AutoParam, r: Range, glide: bool) {
|
||||
let cur = super::mix::current_value(tr, param);
|
||||
let (lo, hi, _) = param.range();
|
||||
let l = tr.lane_mut(param);
|
||||
let before = l.value_at(r.start - 1, cur);
|
||||
let after = l.value_at(r.end, cur);
|
||||
let from = l.value_at(r.start, cur);
|
||||
if glide {
|
||||
l.clear_range(r.start, r.end.saturating_add(2));
|
||||
if r.start > 0 {
|
||||
l.set_point(r.start - 1, before);
|
||||
}
|
||||
l.set_point(r.start, from);
|
||||
l.set_point(r.end, cur);
|
||||
l.set_point(r.end.saturating_add(1), after);
|
||||
} else {
|
||||
let delta = cur - from;
|
||||
let last = l.value_at(r.end - 1, cur);
|
||||
if r.start > 0 {
|
||||
l.set_point(r.start - 1, before);
|
||||
}
|
||||
l.set_point(r.start, from);
|
||||
l.set_point(r.end - 1, last);
|
||||
l.set_point(r.end, after);
|
||||
for pt in l.points.iter_mut().filter(|pt| pt.at >= r.start && pt.at < r.end) {
|
||||
pt.value = (pt.value + delta).clamp(lo, hi);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn trim_glide(e: &mut Engine, p: &Value, glide: bool, pan_only: bool, id: &str) -> Result<Value> {
|
||||
let tracks = tracks_required(e, id, p)?;
|
||||
let r = nonempty_range(e, p, id)?;
|
||||
@@ -863,49 +914,8 @@ fn trim_glide(e: &mut Engine, p: &Value, glide: bool, pan_only: bool, id: &str)
|
||||
let mut lanes = 0;
|
||||
for t in &tracks {
|
||||
let Some(tr) = s.track_mut(*t) else { continue };
|
||||
let mut params: Vec<AutoParam> = tr
|
||||
.automation
|
||||
.iter()
|
||||
.filter(|l| l.enabled && !matches!(l.param, AutoParam::Mute | AutoParam::SendMute(_)))
|
||||
.filter(|l| !pan_only || matches!(l.param, AutoParam::Pan(_)))
|
||||
.map(|l| l.param.clone())
|
||||
.collect();
|
||||
if pan_only {
|
||||
for i in 0..tr.mixer.pan.len().min(2) {
|
||||
let pp = AutoParam::Pan(i as u8);
|
||||
if !params.contains(&pp) {
|
||||
params.push(pp);
|
||||
}
|
||||
}
|
||||
}
|
||||
for param in params {
|
||||
let cur = super::mix::current_value(tr, ¶m);
|
||||
let (lo, hi, _) = param.range();
|
||||
let l = tr.lane_mut(¶m);
|
||||
let before = l.value_at(r.start - 1, cur);
|
||||
let after = l.value_at(r.end, cur);
|
||||
let from = l.value_at(r.start, cur);
|
||||
if glide {
|
||||
l.clear_range(r.start, r.end.saturating_add(2));
|
||||
if r.start > 0 {
|
||||
l.set_point(r.start - 1, before);
|
||||
}
|
||||
l.set_point(r.start, from);
|
||||
l.set_point(r.end, cur);
|
||||
l.set_point(r.end.saturating_add(1), after);
|
||||
} else {
|
||||
let delta = cur - from;
|
||||
let last = l.value_at(r.end - 1, cur);
|
||||
if r.start > 0 {
|
||||
l.set_point(r.start - 1, before);
|
||||
}
|
||||
l.set_point(r.start, from);
|
||||
l.set_point(r.end - 1, last);
|
||||
l.set_point(r.end, after);
|
||||
for pt in l.points.iter_mut().filter(|pt| pt.at >= r.start && pt.at < r.end) {
|
||||
pt.value = (pt.value + delta).clamp(lo, hi);
|
||||
}
|
||||
}
|
||||
for param in trim_params(tr, pan_only) {
|
||||
trim_lane(tr, ¶m, r, glide);
|
||||
lanes += 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -564,18 +564,11 @@ pub fn chord_name(weights: &[f64; 12], bass: usize) -> Option<String> {
|
||||
const MAX_CHORD_BARS: usize = 100_000;
|
||||
const MAX_CHORD_NOTES: usize = 10_000_000;
|
||||
|
||||
fn extract_chords(e: &mut Engine, p: &Value) -> Result<Value> {
|
||||
let id = "event.extract_chords";
|
||||
let (_, t0, t1) = selection_ticks(e, p, id)?;
|
||||
let s = e.session();
|
||||
let picked = tracks_param(e, id, p)?;
|
||||
let mut tracks: Vec<TrackId> = picked.into_iter().filter(|t| s.track(*t).is_some_and(|x| x.kind.is_midi())).collect();
|
||||
if tracks.is_empty() {
|
||||
tracks = s.tracks.iter().filter(|t| t.kind.is_midi()).map(|t| t.id).collect();
|
||||
}
|
||||
// Absolute (tick) notes from every MIDI clip on those tracks, cut to their clip.
|
||||
/// Absolute `(start, end, pitch)` tick notes from every MIDI clip on `tracks`, cut to their clip
|
||||
/// and sorted by start.
|
||||
fn midi_tick_notes(s: &Session, tracks: &[TrackId]) -> Vec<(i64, i64, u8)> {
|
||||
let mut notes: Vec<(i64, i64, u8)> = Vec::new();
|
||||
for t in &tracks {
|
||||
for t in tracks {
|
||||
for c in s.track(*t).map(|tr| tr.clips()).unwrap_or(&[]) {
|
||||
let ClipContent::Midi { sequence } = &c.content else { continue };
|
||||
let base = s.tempo.samples_to_ticks(c.start, s.sample_rate);
|
||||
@@ -590,6 +583,36 @@ fn extract_chords(e: &mut Engine, p: &Value) -> Result<Value> {
|
||||
}
|
||||
}
|
||||
notes.sort_unstable_by_key(|n| n.0);
|
||||
notes
|
||||
}
|
||||
|
||||
/// Duration-weighted pitch classes of the notes sounding in `a..b`, and the lowest pitch among them.
|
||||
fn pitch_class_weights(sounding: &[(i64, i64, u8)], a: i64, b: i64) -> ([f64; 12], Option<u8>) {
|
||||
let mut w = [0.0f64; 12];
|
||||
let mut bass: Option<u8> = None;
|
||||
for (na, nb, pitch) in sounding {
|
||||
let ov = (*nb).min(b) - (*na).max(a);
|
||||
if ov > 0 {
|
||||
if let Some(x) = w.get_mut(usize::from(*pitch) % 12) {
|
||||
*x += ov as f64;
|
||||
}
|
||||
bass = Some(bass.map_or(*pitch, |bp| bp.min(*pitch)));
|
||||
}
|
||||
}
|
||||
(w, bass)
|
||||
}
|
||||
|
||||
fn extract_chords(e: &mut Engine, p: &Value) -> Result<Value> {
|
||||
let id = "event.extract_chords";
|
||||
let (_, t0, t1) = selection_ticks(e, p, id)?;
|
||||
let s = e.session();
|
||||
let picked = tracks_param(e, id, p)?;
|
||||
let mut tracks: Vec<TrackId> = picked.into_iter().filter(|t| s.track(*t).is_some_and(|x| x.kind.is_midi())).collect();
|
||||
if tracks.is_empty() {
|
||||
tracks = s.tracks.iter().filter(|t| t.kind.is_midi()).map(|t| t.id).collect();
|
||||
}
|
||||
// Absolute (tick) notes from every MIDI clip on those tracks, cut to their clip.
|
||||
let notes = midi_tick_notes(s, &tracks);
|
||||
let mut bar = s.tempo.bar_beat_at_tick(t0).bar;
|
||||
let mut sounding: Vec<(i64, i64, u8)> = Vec::new();
|
||||
let (mut next, mut bars, mut heard) = (0, 0, 0);
|
||||
@@ -616,17 +639,7 @@ fn extract_chords(e: &mut Engine, p: &Value) -> Result<Value> {
|
||||
}
|
||||
bars += 1;
|
||||
heard += sounding.len();
|
||||
let mut w = [0.0f64; 12];
|
||||
let mut bass: Option<u8> = None;
|
||||
for (na, nb, pitch) in &sounding {
|
||||
let ov = (*nb).min(b) - (*na).max(a);
|
||||
if ov > 0 {
|
||||
if let Some(x) = w.get_mut(usize::from(*pitch) % 12) {
|
||||
*x += ov as f64;
|
||||
}
|
||||
bass = Some(bass.map_or(*pitch, |bp| bp.min(*pitch)));
|
||||
}
|
||||
}
|
||||
let (w, bass) = pitch_class_weights(&sounding, a, b);
|
||||
if let Some(name) = bass.and_then(|bp| chord_name(&w, usize::from(bp) % 12)) {
|
||||
let at = s.tempo.tick_to_samples(a, s.sample_rate);
|
||||
chords.push(json!({"bar": bar, "chord": name, "at": at}));
|
||||
|
||||
@@ -612,6 +612,76 @@ fn env_db(env: &[(i64, f32)], at: i64) -> f32 {
|
||||
prev.map_or(0.0, |(_, v)| v)
|
||||
}
|
||||
|
||||
/// Fold the volume `lane` into the gain envelope of clip `c` over the selection `r`.
|
||||
fn bake_volume_into_clip(c: &mut soundcraft_model::Clip, lane: &soundcraft_model::AutomationLane, base: f32, r: soundcraft_time::Range) {
|
||||
let delta = |at: i64| lane.value_at(at, base) - base;
|
||||
let sel0 = r.start.max(c.start);
|
||||
let sel1 = r.end.min(c.end());
|
||||
if sel1 <= sel0 {
|
||||
return;
|
||||
}
|
||||
let rel = |at: i64| at - c.start;
|
||||
let rel0 = rel(sel0);
|
||||
let rel1 = rel(sel1);
|
||||
// Later writes win, so the selection anchors replace any point they land on.
|
||||
let mut pts: Vec<(i64, f32)> =
|
||||
c.gain_env.iter().copied().filter(|&(at, _)| at < rel0.saturating_sub(1) || (sel1 < c.end() && at > rel1)).collect();
|
||||
if rel0 > 0 {
|
||||
pts.push((rel0 - 1, env_db(&c.gain_env, rel0 - 1)));
|
||||
}
|
||||
pts.push((rel0, env_db(&c.gain_env, rel0) + delta(sel0)));
|
||||
for pt in lane.points.iter().filter(|pt| pt.at > sel0 && pt.at < sel1) {
|
||||
let at = rel(pt.at);
|
||||
pts.push((at, env_db(&c.gain_env, at) + delta(pt.at)));
|
||||
}
|
||||
// The clip's own gain points inside the selection keep their shape, offset by the lane.
|
||||
for &(at, _) in c.gain_env.iter().filter(|&&(at, _)| at > rel0 && at < rel1) {
|
||||
pts.push((at, env_db(&c.gain_env, at) + delta(c.start.saturating_add(at))));
|
||||
}
|
||||
if sel1 - 1 > sel0 {
|
||||
let at = rel(sel1 - 1);
|
||||
pts.push((at, env_db(&c.gain_env, at) + delta(sel1 - 1)));
|
||||
}
|
||||
if sel1 < c.end() {
|
||||
pts.push((rel1, env_db(&c.gain_env, rel1)));
|
||||
}
|
||||
pts.sort_by_key(|p| p.0);
|
||||
let mut env = Vec::new();
|
||||
for p in pts {
|
||||
if env.last().is_some_and(|q: &(i64, f32)| q.0 == p.0) {
|
||||
env.pop();
|
||||
}
|
||||
env.push(p);
|
||||
}
|
||||
c.gain_env = env;
|
||||
}
|
||||
|
||||
/// Clear the volume lane over `r` when the clips all sit inside it, else hold the fader there.
|
||||
fn reset_volume_lane(tr: &mut soundcraft_model::Track, lane: &soundcraft_model::AutomationLane, base: f32, r: soundcraft_time::Range) {
|
||||
// Clearing the lane is only safe when no audio plays outside the selection; otherwise the
|
||||
// held edge values outside would fall back to the fader level.
|
||||
let clips_inside = tr.clips().iter().all(|c| c.start >= r.start && c.end() <= r.end);
|
||||
let covers_all = clips_inside && lane.points.iter().all(|pt| pt.at >= r.start && pt.at < r.end);
|
||||
let lane_m = tr.lane_mut(&AutoParam::Volume);
|
||||
if covers_all {
|
||||
lane_m.clear_range(r.start, r.end);
|
||||
} else {
|
||||
// Hold the fader across the selection and anchor the samples on either side, so the
|
||||
// curve outside does not interpolate through the gap or keep the old level inside.
|
||||
let before = (r.start > 0).then(|| lane.value_at(r.start - 1, base));
|
||||
let after = lane.value_at(r.end, base);
|
||||
lane_m.clear_range(r.start, r.end);
|
||||
if let Some(v) = before {
|
||||
lane_m.set_point(r.start - 1, v);
|
||||
}
|
||||
lane_m.set_point(r.start, base);
|
||||
if r.end - 1 > r.start {
|
||||
lane_m.set_point(r.end - 1, base);
|
||||
}
|
||||
lane_m.set_point(r.end, after);
|
||||
}
|
||||
}
|
||||
|
||||
fn vol_to_clip_gain(e: &mut Engine, p: &Value) -> Result<Value> {
|
||||
let tracks = tracks_required(e, "automation.volume_to_clip_gain", p)?;
|
||||
let r = range_param(e, "automation.volume_to_clip_gain", p)?;
|
||||
@@ -620,72 +690,12 @@ fn vol_to_clip_gain(e: &mut Engine, p: &Value) -> Result<Value> {
|
||||
let Some(tr) = s.track_mut(*t) else { continue };
|
||||
let Some(lane) = tr.lane(&AutoParam::Volume).cloned().filter(|l| !l.points.is_empty()) else { continue };
|
||||
let base = tr.mixer.volume_db;
|
||||
let delta = |at: i64| lane.value_at(at, base) - base;
|
||||
if let Some(pl) = tr.playlist_mut() {
|
||||
for c in pl.clips.iter_mut().filter(|c| c.range().overlaps(&r)) {
|
||||
let sel0 = r.start.max(c.start);
|
||||
let sel1 = r.end.min(c.end());
|
||||
if sel1 <= sel0 {
|
||||
continue;
|
||||
}
|
||||
let rel = |at: i64| at - c.start;
|
||||
let rel0 = rel(sel0);
|
||||
let rel1 = rel(sel1);
|
||||
// Later writes win, so the selection anchors replace any point they land on.
|
||||
let mut pts: Vec<(i64, f32)> =
|
||||
c.gain_env.iter().copied().filter(|&(at, _)| at < rel0.saturating_sub(1) || (sel1 < c.end() && at > rel1)).collect();
|
||||
if rel0 > 0 {
|
||||
pts.push((rel0 - 1, env_db(&c.gain_env, rel0 - 1)));
|
||||
}
|
||||
pts.push((rel0, env_db(&c.gain_env, rel0) + delta(sel0)));
|
||||
for pt in lane.points.iter().filter(|pt| pt.at > sel0 && pt.at < sel1) {
|
||||
let at = rel(pt.at);
|
||||
pts.push((at, env_db(&c.gain_env, at) + delta(pt.at)));
|
||||
}
|
||||
// The clip's own gain points inside the selection keep their shape, offset by the lane.
|
||||
for &(at, _) in c.gain_env.iter().filter(|&&(at, _)| at > rel0 && at < rel1) {
|
||||
pts.push((at, env_db(&c.gain_env, at) + delta(c.start.saturating_add(at))));
|
||||
}
|
||||
if sel1 - 1 > sel0 {
|
||||
let at = rel(sel1 - 1);
|
||||
pts.push((at, env_db(&c.gain_env, at) + delta(sel1 - 1)));
|
||||
}
|
||||
if sel1 < c.end() {
|
||||
pts.push((rel1, env_db(&c.gain_env, rel1)));
|
||||
}
|
||||
pts.sort_by_key(|p| p.0);
|
||||
let mut env = Vec::new();
|
||||
for p in pts {
|
||||
if env.last().is_some_and(|q: &(i64, f32)| q.0 == p.0) {
|
||||
env.pop();
|
||||
}
|
||||
env.push(p);
|
||||
}
|
||||
c.gain_env = env;
|
||||
bake_volume_into_clip(c, &lane, base, r);
|
||||
}
|
||||
}
|
||||
// Clearing the lane is only safe when no audio plays outside the selection; otherwise the
|
||||
// held edge values outside would fall back to the fader level.
|
||||
let clips_inside = tr.clips().iter().all(|c| c.start >= r.start && c.end() <= r.end);
|
||||
let covers_all = clips_inside && lane.points.iter().all(|pt| pt.at >= r.start && pt.at < r.end);
|
||||
let lane_m = tr.lane_mut(&AutoParam::Volume);
|
||||
if covers_all {
|
||||
lane_m.clear_range(r.start, r.end);
|
||||
} else {
|
||||
// Hold the fader across the selection and anchor the samples on either side, so the
|
||||
// curve outside does not interpolate through the gap or keep the old level inside.
|
||||
let before = (r.start > 0).then(|| lane.value_at(r.start - 1, base));
|
||||
let after = lane.value_at(r.end, base);
|
||||
lane_m.clear_range(r.start, r.end);
|
||||
if let Some(v) = before {
|
||||
lane_m.set_point(r.start - 1, v);
|
||||
}
|
||||
lane_m.set_point(r.start, base);
|
||||
if r.end - 1 > r.start {
|
||||
lane_m.set_point(r.end - 1, base);
|
||||
}
|
||||
lane_m.set_point(r.end, after);
|
||||
}
|
||||
reset_volume_lane(tr, &lane, base, r);
|
||||
}
|
||||
Ok(json!({}))
|
||||
}
|
||||
|
||||
@@ -221,6 +221,23 @@ fn io_state(s: &Session) -> Value {
|
||||
json!({"busses": s.busses, "outputs": s.outputs})
|
||||
}
|
||||
|
||||
/// Disconnect every track input, output and send that used bus `bid`.
|
||||
fn unroute_bus(s: &mut Session, bid: BusId) {
|
||||
for t in &mut s.tracks {
|
||||
if t.mixer.input == Route::Bus(bid) {
|
||||
t.mixer.input = Route::None;
|
||||
}
|
||||
if t.mixer.output == Route::Bus(bid) {
|
||||
t.mixer.output = Route::None;
|
||||
}
|
||||
for slot in &mut t.mixer.sends {
|
||||
if slot.as_ref().is_some_and(|x| x.target == Route::Bus(bid)) {
|
||||
*slot = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn io_setup(e: &mut Engine, p: &Value) -> Result<Value> {
|
||||
let id = "setup.io";
|
||||
let action = str_param(p, "action").unwrap_or("list");
|
||||
@@ -252,19 +269,7 @@ fn io_setup(e: &mut Engine, p: &Value) -> Result<Value> {
|
||||
"delete_bus" => {
|
||||
let bid: BusId = s.bus_by_name(&name).map(|b| b.id).ok_or_else(|| bad(id, format!("no bus `{name}`")))?;
|
||||
s.busses.retain(|b| b.id != bid);
|
||||
for t in &mut s.tracks {
|
||||
if t.mixer.input == Route::Bus(bid) {
|
||||
t.mixer.input = Route::None;
|
||||
}
|
||||
if t.mixer.output == Route::Bus(bid) {
|
||||
t.mixer.output = Route::None;
|
||||
}
|
||||
for slot in &mut t.mixer.sends {
|
||||
if slot.as_ref().is_some_and(|x| x.target == Route::Bus(bid)) {
|
||||
*slot = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
unroute_bus(s, bid);
|
||||
}
|
||||
"create_output" => {
|
||||
if s.outputs.iter().any(|o| o.name.eq_ignore_ascii_case(&name)) {
|
||||
|
||||
@@ -635,6 +635,35 @@ fn change_width(e: &mut Engine, p: &Value) -> Result<Value> {
|
||||
Ok(json!({"format": f}))
|
||||
}
|
||||
|
||||
/// The nearest ancestor folder of `folder` that is not itself being deleted (walks at most 64 levels).
|
||||
fn surviving_parent(s: &Session, deleted: &[TrackId], folder: TrackId) -> Option<TrackId> {
|
||||
let mut parent = s.track(folder).and_then(|folder| folder.folder);
|
||||
for _ in 0..64 {
|
||||
match parent {
|
||||
Some(candidate) if deleted.contains(&candidate) => parent = s.track(candidate).and_then(|folder| folder.folder),
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
parent
|
||||
}
|
||||
|
||||
/// Clear inputs and sends that used the removed bus; outputs that used it move to `replacement`.
|
||||
fn reroute_deleted_bus(s: &mut Session, bus: soundcraft_model::BusId, replacement: &Route) {
|
||||
for track in &mut s.tracks {
|
||||
if track.mixer.input == Route::Bus(bus) {
|
||||
track.mixer.input = Route::None;
|
||||
}
|
||||
if track.mixer.output == Route::Bus(bus) {
|
||||
track.mixer.output = replacement.clone();
|
||||
}
|
||||
for send in &mut track.mixer.sends {
|
||||
if send.as_ref().is_some_and(|send| send.target == Route::Bus(bus)) {
|
||||
*send = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn delete(e: &mut Engine, p: &Value) -> Result<Value> {
|
||||
let tracks = tracks_required(e, "track.delete", p)?;
|
||||
let s = e.session_mut();
|
||||
@@ -642,13 +671,7 @@ fn delete(e: &mut Engine, p: &Value) -> Result<Value> {
|
||||
.iter()
|
||||
.filter_map(|folder| {
|
||||
let bus = owned_folder_bus(s, *folder)?;
|
||||
let mut parent = s.track(*folder).and_then(|folder| folder.folder);
|
||||
for _ in 0..64 {
|
||||
match parent {
|
||||
Some(candidate) if tracks.contains(&candidate) => parent = s.track(candidate).and_then(|folder| folder.folder),
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
let parent = surviving_parent(s, &tracks, *folder);
|
||||
Some((bus, parent.and_then(|parent| folder_bus(s, parent)).unwrap_or(Route::Main)))
|
||||
})
|
||||
.collect();
|
||||
@@ -660,13 +683,7 @@ fn delete(e: &mut Engine, p: &Value) -> Result<Value> {
|
||||
continue;
|
||||
}
|
||||
let Some(folder) = child.folder.filter(|folder| tracks.contains(folder)) else { continue };
|
||||
let mut parent = s.track(folder).and_then(|folder| folder.folder);
|
||||
for _ in 0..64 {
|
||||
match parent {
|
||||
Some(candidate) if tracks.contains(&candidate) => parent = s.track(candidate).and_then(|folder| folder.folder),
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
let parent = surviving_parent(s, &tracks, folder);
|
||||
promoted.push((child.id, folder_bus(s, folder), parent, parent.and_then(|parent| folder_bus(s, parent))));
|
||||
}
|
||||
for (child, old_bus, parent, parent_bus) in promoted {
|
||||
@@ -684,19 +701,7 @@ fn delete(e: &mut Engine, p: &Value) -> Result<Value> {
|
||||
}
|
||||
for (bus, replacement) in removed_busses {
|
||||
s.busses.retain(|candidate| candidate.id != bus);
|
||||
for track in &mut s.tracks {
|
||||
if track.mixer.input == Route::Bus(bus) {
|
||||
track.mixer.input = Route::None;
|
||||
}
|
||||
if track.mixer.output == Route::Bus(bus) {
|
||||
track.mixer.output = replacement.clone();
|
||||
}
|
||||
for send in &mut track.mixer.sends {
|
||||
if send.as_ref().is_some_and(|send| send.target == Route::Bus(bus)) {
|
||||
*send = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
reroute_deleted_bus(s, bus, &replacement);
|
||||
}
|
||||
Ok(json!({"deleted": tracks.len()}))
|
||||
}
|
||||
|
||||
+180
-154
@@ -231,6 +231,84 @@ fn close_note(open: &mut OpenNotes, notes: &mut Vec<Note>, channel: u8, pitch: u
|
||||
notes.push(Note { pitch, velocity, release_velocity: release, channel, start, length });
|
||||
}
|
||||
|
||||
fn data_byte(cur: &mut Cursor<'_>, event_at: usize) -> Result<u8, MidiError> {
|
||||
let d = cur.u8()?;
|
||||
if d & 0x80 != 0 {
|
||||
return Err(MidiError::Invalid { offset: event_at, reason: "status byte where a data byte was expected" });
|
||||
}
|
||||
Ok(d)
|
||||
}
|
||||
|
||||
/// Apply a meta event (other than End of Track) to the track name and the file-wide maps.
|
||||
fn apply_meta(kind: u8, data: &[u8], tick: i64, name: &mut Option<String>, g: &mut Globals) {
|
||||
match kind {
|
||||
0x03 if name.is_none() => {
|
||||
*name = Some(String::from_utf8_lossy(data).into_owned());
|
||||
}
|
||||
0x06 => g.markers.push((tick, String::from_utf8_lossy(data).into_owned())),
|
||||
0x51 => {
|
||||
if let [a, b, c] = data {
|
||||
let us = (u32::from(*a) << 16) | (u32::from(*b) << 8) | u32::from(*c);
|
||||
if us > 0 {
|
||||
g.tempos.push((tick, 60_000_000.0 / f64::from(us)));
|
||||
}
|
||||
}
|
||||
}
|
||||
0x58 => {
|
||||
if let [nn, dd, ..] = data
|
||||
&& *nn > 0
|
||||
&& *dd <= 7
|
||||
{
|
||||
g.meters.push((tick, *nn, 1u8 << *dd));
|
||||
}
|
||||
}
|
||||
0x59 => {
|
||||
if let [sf, mi, ..] = data {
|
||||
let sf = *sf as i8;
|
||||
if (-7..=7).contains(&sf) {
|
||||
g.key_sigs.push((tick, sf, *mi == 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply a channel voice event `(status, d1, d2)` at `tick`.
|
||||
fn channel_event(open: &mut OpenNotes, seq: &mut Sequence, (status, d1, d2): (u8, u8, u8), tick: i64) {
|
||||
let channel = status & 0x0F;
|
||||
match status & 0xF0 {
|
||||
0x80 => close_note(open, &mut seq.notes, channel, d1, tick, d2),
|
||||
0x90 if d2 == 0 => close_note(open, &mut seq.notes, channel, d1, tick, DEFAULT_RELEASE),
|
||||
0x90 => {
|
||||
if let Some(stack) = open.get_mut(usize::from(channel) * 128 + usize::from(d1)) {
|
||||
stack.push_back((tick, d2));
|
||||
}
|
||||
}
|
||||
0xA0 => seq.ctrls.push(CtrlEvent { tick, channel, kind: CtrlKind::PolyPressure(d1), value: i32::from(d2) }),
|
||||
0xB0 => seq.ctrls.push(CtrlEvent { tick, channel, kind: CtrlKind::Cc(d1), value: i32::from(d2) }),
|
||||
0xC0 => seq.ctrls.push(CtrlEvent { tick, channel, kind: CtrlKind::Program, value: i32::from(d1) }),
|
||||
0xD0 => seq.ctrls.push(CtrlEvent { tick, channel, kind: CtrlKind::ChannelPressure, value: i32::from(d1) }),
|
||||
_ => {
|
||||
// 0xE0 pitch bend: LSB then MSB, centred on 8192.
|
||||
let v = (i32::from(d2) << 7 | i32::from(d1)) - 8192;
|
||||
seq.ctrls.push(CtrlEvent { tick, channel, kind: CtrlKind::PitchBend, value: v });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Close hanging notes at the end of the track.
|
||||
fn close_hanging(open: &mut OpenNotes, seq: &mut Sequence, tick: i64) {
|
||||
for (key, stack) in open.iter_mut().enumerate() {
|
||||
let channel = u8::try_from(key / 128).unwrap_or(0);
|
||||
let pitch = u8::try_from(key % 128).unwrap_or(0);
|
||||
for (start, velocity) in stack.drain(..) {
|
||||
let length = tick.saturating_sub(start).max(1);
|
||||
seq.notes.push(Note { pitch, velocity, release_velocity: DEFAULT_RELEASE, channel, start, length });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_track(body: &[u8], base: usize, conv: TickConv, g: &mut Globals, budget: &mut usize) -> Result<SmfTrack, MidiError> {
|
||||
let mut cur = Cursor { data: body, pos: 0, base };
|
||||
let mut name: Option<String> = None;
|
||||
@@ -265,38 +343,10 @@ fn read_track(body: &[u8], base: usize, conv: TickConv, g: &mut Globals, budget:
|
||||
let kind = cur.u8()?;
|
||||
let len = cur.vlq()?;
|
||||
let data = cur.bytes(usize::try_from(len).unwrap_or(usize::MAX))?;
|
||||
match kind {
|
||||
0x2F => break, // End of Track
|
||||
0x03 if name.is_none() => {
|
||||
name = Some(String::from_utf8_lossy(data).into_owned());
|
||||
}
|
||||
0x06 => g.markers.push((tick, String::from_utf8_lossy(data).into_owned())),
|
||||
0x51 => {
|
||||
if let [a, b, c] = data {
|
||||
let us = (u32::from(*a) << 16) | (u32::from(*b) << 8) | u32::from(*c);
|
||||
if us > 0 {
|
||||
g.tempos.push((tick, 60_000_000.0 / f64::from(us)));
|
||||
}
|
||||
}
|
||||
}
|
||||
0x58 => {
|
||||
if let [nn, dd, ..] = data
|
||||
&& *nn > 0
|
||||
&& *dd <= 7
|
||||
{
|
||||
g.meters.push((tick, *nn, 1u8 << *dd));
|
||||
}
|
||||
}
|
||||
0x59 => {
|
||||
if let [sf, mi, ..] = data {
|
||||
let sf = *sf as i8;
|
||||
if (-7..=7).contains(&sf) {
|
||||
g.key_sigs.push((tick, sf, *mi == 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
if kind == 0x2F {
|
||||
break; // End of Track
|
||||
}
|
||||
apply_meta(kind, data, tick, &mut name, g);
|
||||
}
|
||||
0xF0 | 0xF7 => {
|
||||
running = None;
|
||||
@@ -305,53 +355,16 @@ fn read_track(body: &[u8], base: usize, conv: TickConv, g: &mut Globals, budget:
|
||||
}
|
||||
0x80..=0xEF => {
|
||||
running = Some(status);
|
||||
let channel = status & 0x0F;
|
||||
let d1 = cur.u8()?;
|
||||
if d1 & 0x80 != 0 {
|
||||
return Err(MidiError::Invalid { offset: event_at, reason: "status byte where a data byte was expected" });
|
||||
}
|
||||
let d1 = data_byte(&mut cur, event_at)?;
|
||||
let needs_two = !matches!(status & 0xF0, 0xC0 | 0xD0);
|
||||
let d2 = if needs_two {
|
||||
let d = cur.u8()?;
|
||||
if d & 0x80 != 0 {
|
||||
return Err(MidiError::Invalid { offset: event_at, reason: "status byte where a data byte was expected" });
|
||||
}
|
||||
d
|
||||
} else {
|
||||
0
|
||||
};
|
||||
match status & 0xF0 {
|
||||
0x80 => close_note(&mut open, &mut seq.notes, channel, d1, tick, d2),
|
||||
0x90 if d2 == 0 => close_note(&mut open, &mut seq.notes, channel, d1, tick, DEFAULT_RELEASE),
|
||||
0x90 => {
|
||||
if let Some(stack) = open.get_mut(usize::from(channel) * 128 + usize::from(d1)) {
|
||||
stack.push_back((tick, d2));
|
||||
}
|
||||
}
|
||||
0xA0 => seq.ctrls.push(CtrlEvent { tick, channel, kind: CtrlKind::PolyPressure(d1), value: i32::from(d2) }),
|
||||
0xB0 => seq.ctrls.push(CtrlEvent { tick, channel, kind: CtrlKind::Cc(d1), value: i32::from(d2) }),
|
||||
0xC0 => seq.ctrls.push(CtrlEvent { tick, channel, kind: CtrlKind::Program, value: i32::from(d1) }),
|
||||
0xD0 => seq.ctrls.push(CtrlEvent { tick, channel, kind: CtrlKind::ChannelPressure, value: i32::from(d1) }),
|
||||
_ => {
|
||||
// 0xE0 pitch bend: LSB then MSB, centred on 8192.
|
||||
let v = (i32::from(d2) << 7 | i32::from(d1)) - 8192;
|
||||
seq.ctrls.push(CtrlEvent { tick, channel, kind: CtrlKind::PitchBend, value: v });
|
||||
}
|
||||
}
|
||||
let d2 = if needs_two { data_byte(&mut cur, event_at)? } else { 0 };
|
||||
channel_event(&mut open, &mut seq, (status, d1, d2), tick);
|
||||
}
|
||||
_ => return Err(MidiError::Invalid { offset: event_at, reason: "system real-time/common status in a track" }),
|
||||
}
|
||||
}
|
||||
|
||||
// Close hanging notes at the end of the track.
|
||||
for (key, stack) in open.iter_mut().enumerate() {
|
||||
let channel = u8::try_from(key / 128).unwrap_or(0);
|
||||
let pitch = u8::try_from(key % 128).unwrap_or(0);
|
||||
for (start, velocity) in stack.drain(..) {
|
||||
let length = tick.saturating_sub(start).max(1);
|
||||
seq.notes.push(Note { pitch, velocity, release_velocity: DEFAULT_RELEASE, channel, start, length });
|
||||
}
|
||||
}
|
||||
close_hanging(&mut open, &mut seq, tick);
|
||||
seq.sort();
|
||||
Ok(SmfTrack { name: name.unwrap_or_default(), sequence: seq })
|
||||
}
|
||||
@@ -434,25 +447,8 @@ fn write_track(out: &mut Vec<u8>, mut events: Vec<WEvent>) -> Result<(), MidiErr
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Writes a format-1 Standard MIDI File at [`PPQ`] (960) ticks per quarter note.
|
||||
///
|
||||
/// Track 1 is a conductor track holding tempos, meters, key signatures and markers; every
|
||||
/// [`SmfTrack`] follows as its own track. Negative ticks are clamped to 0, data values are
|
||||
/// clamped to their MIDI ranges, and note-on velocity 0 is written as 1 (0 would mean
|
||||
/// note-off). At each tick note-offs are written before controllers, then note-ons.
|
||||
///
|
||||
/// Errors: invalid tempo (non-finite or <= 0 bpm), meter denominator that is not a power of two
|
||||
/// (1..=128), more than 65535 tracks, or a gap between events beyond the 28-bit delta limit.
|
||||
pub fn write_smf(smf: &Smf) -> Result<Vec<u8>, MidiError> {
|
||||
let ntrks = u16::try_from(smf.tracks.len().saturating_add(1)).map_err(|_| MidiError::Write("more than 65535 tracks".into()))?;
|
||||
let mut out = Vec::new();
|
||||
out.extend_from_slice(b"MThd");
|
||||
out.extend_from_slice(&6u32.to_be_bytes());
|
||||
out.extend_from_slice(&1u16.to_be_bytes());
|
||||
out.extend_from_slice(&ntrks.to_be_bytes());
|
||||
out.extend_from_slice(&u16::try_from(PPQ).unwrap_or(960).to_be_bytes());
|
||||
|
||||
// Conductor track.
|
||||
/// The conductor track: tempos, meters, key signatures and markers.
|
||||
fn conductor_events(smf: &Smf) -> Result<Vec<WEvent>, MidiError> {
|
||||
let mut ev = Vec::new();
|
||||
for &(t, bpm) in &smf.tempos {
|
||||
if !bpm.is_finite() || bpm <= 0.0 {
|
||||
@@ -476,75 +472,105 @@ pub fn write_smf(smf: &Smf) -> Result<Vec<u8>, MidiError> {
|
||||
for (t, text) in &smf.markers {
|
||||
ev.push(WEvent { tick: clamp_tick(*t), order: 3, bytes: meta(0x06, text.as_bytes())? });
|
||||
}
|
||||
write_track(&mut out, ev)?;
|
||||
Ok(ev)
|
||||
}
|
||||
|
||||
for track in &smf.tracks {
|
||||
let mut ev = Vec::new();
|
||||
if !track.name.is_empty() {
|
||||
ev.push(WEvent { tick: 0, order: 0, bytes: meta(0x03, track.name.as_bytes())? });
|
||||
}
|
||||
let notes = &track.sequence.notes;
|
||||
let mut ends: Vec<u64> = notes.iter().map(|n| clamp_tick(n.end()).max(clamp_tick(n.start).saturating_add(1))).collect();
|
||||
let mut dropped = vec![false; notes.len()];
|
||||
// The reader pairs offs with ons first-in first-out, so a note still sounding when the next
|
||||
// same-pitch note starts would swap ends with it: trim it to that start. When two same-pitch
|
||||
// notes start on the same tick they cannot be told apart, so only the longer one is written.
|
||||
let mut order: Vec<usize> = (0..notes.len()).collect();
|
||||
order.sort_by_key(|&i| {
|
||||
notes.get(i).map_or((0, 0, 0, std::cmp::Reverse(0)), |n| {
|
||||
(n.channel & 0x0F, n.pitch.min(127), clamp_tick(n.start), std::cmp::Reverse(ends.get(i).copied().unwrap_or(0)))
|
||||
})
|
||||
});
|
||||
let mut prev: Option<usize> = None;
|
||||
for &b in &order {
|
||||
let Some(nb) = notes.get(b) else { continue };
|
||||
if let Some(a) = prev
|
||||
&& let Some(na) = notes.get(a)
|
||||
&& na.channel & 0x0F == nb.channel & 0x0F
|
||||
&& na.pitch.min(127) == nb.pitch.min(127)
|
||||
{
|
||||
let (sa, sb) = (clamp_tick(na.start), clamp_tick(nb.start));
|
||||
if sa == sb {
|
||||
if let Some(d) = dropped.get_mut(b) {
|
||||
*d = true;
|
||||
}
|
||||
continue;
|
||||
/// Trim notes still sounding when the next same-pitch note starts, and drop duplicates.
|
||||
fn trim_overlaps(notes: &[Note], ends: &mut [u64], dropped: &mut [bool]) {
|
||||
// The reader pairs offs with ons first-in first-out, so a note still sounding when the next
|
||||
// same-pitch note starts would swap ends with it: trim it to that start. When two same-pitch
|
||||
// notes start on the same tick they cannot be told apart, so only the longer one is written.
|
||||
let mut order: Vec<usize> = (0..notes.len()).collect();
|
||||
order.sort_by_key(|&i| {
|
||||
notes.get(i).map_or((0, 0, 0, std::cmp::Reverse(0)), |n| {
|
||||
(n.channel & 0x0F, n.pitch.min(127), clamp_tick(n.start), std::cmp::Reverse(ends.get(i).copied().unwrap_or(0)))
|
||||
})
|
||||
});
|
||||
let mut prev: Option<usize> = None;
|
||||
for &b in &order {
|
||||
let Some(nb) = notes.get(b) else { continue };
|
||||
if let Some(a) = prev
|
||||
&& let Some(na) = notes.get(a)
|
||||
&& na.channel & 0x0F == nb.channel & 0x0F
|
||||
&& na.pitch.min(127) == nb.pitch.min(127)
|
||||
{
|
||||
let (sa, sb) = (clamp_tick(na.start), clamp_tick(nb.start));
|
||||
if sa == sb {
|
||||
if let Some(d) = dropped.get_mut(b) {
|
||||
*d = true;
|
||||
}
|
||||
if let Some(ea) = ends.get_mut(a)
|
||||
&& sb < *ea
|
||||
{
|
||||
*ea = sb;
|
||||
}
|
||||
}
|
||||
prev = Some(b);
|
||||
}
|
||||
for (i, n) in notes.iter().enumerate() {
|
||||
if dropped.get(i).copied().unwrap_or(false) {
|
||||
continue;
|
||||
}
|
||||
let ch = n.channel & 0x0F;
|
||||
let pitch = n.pitch.min(127);
|
||||
let start = clamp_tick(n.start);
|
||||
let end = ends.get(i).copied().unwrap_or(start.saturating_add(1));
|
||||
ev.push(WEvent { tick: start, order: 3, bytes: vec![0x90 | ch, pitch, n.velocity.clamp(1, 127)] });
|
||||
ev.push(WEvent { tick: end, order: 1, bytes: vec![0x80 | ch, pitch, n.release_velocity.min(127)] });
|
||||
if let Some(ea) = ends.get_mut(a)
|
||||
&& sb < *ea
|
||||
{
|
||||
*ea = sb;
|
||||
}
|
||||
}
|
||||
for c in &track.sequence.ctrls {
|
||||
let ch = c.channel & 0x0F;
|
||||
let v7 = c.value.clamp(0, 127) as u8;
|
||||
let bytes = match c.kind {
|
||||
CtrlKind::Cc(num) => vec![0xB0 | ch, num.min(127), v7],
|
||||
CtrlKind::Program => vec![0xC0 | ch, v7],
|
||||
CtrlKind::ChannelPressure => vec![0xD0 | ch, v7],
|
||||
CtrlKind::PolyPressure(p) => vec![0xA0 | ch, p.min(127), v7],
|
||||
CtrlKind::PitchBend => {
|
||||
let v = (c.value.saturating_add(8192)).clamp(0, 16383);
|
||||
vec![0xE0 | ch, (v & 0x7F) as u8, ((v >> 7) & 0x7F) as u8]
|
||||
}
|
||||
};
|
||||
ev.push(WEvent { tick: clamp_tick(c.tick), order: 2, bytes });
|
||||
prev = Some(b);
|
||||
}
|
||||
}
|
||||
|
||||
/// One track's events: its name, notes (with overlap trimming) and controllers.
|
||||
fn track_events(track: &SmfTrack) -> Result<Vec<WEvent>, MidiError> {
|
||||
let mut ev = Vec::new();
|
||||
if !track.name.is_empty() {
|
||||
ev.push(WEvent { tick: 0, order: 0, bytes: meta(0x03, track.name.as_bytes())? });
|
||||
}
|
||||
let notes = &track.sequence.notes;
|
||||
let mut ends: Vec<u64> = notes.iter().map(|n| clamp_tick(n.end()).max(clamp_tick(n.start).saturating_add(1))).collect();
|
||||
let mut dropped = vec![false; notes.len()];
|
||||
trim_overlaps(notes, &mut ends, &mut dropped);
|
||||
for (i, n) in notes.iter().enumerate() {
|
||||
if dropped.get(i).copied().unwrap_or(false) {
|
||||
continue;
|
||||
}
|
||||
write_track(&mut out, ev)?;
|
||||
let ch = n.channel & 0x0F;
|
||||
let pitch = n.pitch.min(127);
|
||||
let start = clamp_tick(n.start);
|
||||
let end = ends.get(i).copied().unwrap_or(start.saturating_add(1));
|
||||
ev.push(WEvent { tick: start, order: 3, bytes: vec![0x90 | ch, pitch, n.velocity.clamp(1, 127)] });
|
||||
ev.push(WEvent { tick: end, order: 1, bytes: vec![0x80 | ch, pitch, n.release_velocity.min(127)] });
|
||||
}
|
||||
for c in &track.sequence.ctrls {
|
||||
let ch = c.channel & 0x0F;
|
||||
let v7 = c.value.clamp(0, 127) as u8;
|
||||
let bytes = match c.kind {
|
||||
CtrlKind::Cc(num) => vec![0xB0 | ch, num.min(127), v7],
|
||||
CtrlKind::Program => vec![0xC0 | ch, v7],
|
||||
CtrlKind::ChannelPressure => vec![0xD0 | ch, v7],
|
||||
CtrlKind::PolyPressure(p) => vec![0xA0 | ch, p.min(127), v7],
|
||||
CtrlKind::PitchBend => {
|
||||
let v = (c.value.saturating_add(8192)).clamp(0, 16383);
|
||||
vec![0xE0 | ch, (v & 0x7F) as u8, ((v >> 7) & 0x7F) as u8]
|
||||
}
|
||||
};
|
||||
ev.push(WEvent { tick: clamp_tick(c.tick), order: 2, bytes });
|
||||
}
|
||||
Ok(ev)
|
||||
}
|
||||
|
||||
/// Writes a format-1 Standard MIDI File at [`PPQ`] (960) ticks per quarter note.
|
||||
///
|
||||
/// Track 1 is a conductor track holding tempos, meters, key signatures and markers; every
|
||||
/// [`SmfTrack`] follows as its own track. Negative ticks are clamped to 0, data values are
|
||||
/// clamped to their MIDI ranges, and note-on velocity 0 is written as 1 (0 would mean
|
||||
/// note-off). At each tick note-offs are written before controllers, then note-ons.
|
||||
///
|
||||
/// Errors: invalid tempo (non-finite or <= 0 bpm), meter denominator that is not a power of two
|
||||
/// (1..=128), more than 65535 tracks, or a gap between events beyond the 28-bit delta limit.
|
||||
pub fn write_smf(smf: &Smf) -> Result<Vec<u8>, MidiError> {
|
||||
let ntrks = u16::try_from(smf.tracks.len().saturating_add(1)).map_err(|_| MidiError::Write("more than 65535 tracks".into()))?;
|
||||
let mut out = Vec::new();
|
||||
out.extend_from_slice(b"MThd");
|
||||
out.extend_from_slice(&6u32.to_be_bytes());
|
||||
out.extend_from_slice(&1u16.to_be_bytes());
|
||||
out.extend_from_slice(&ntrks.to_be_bytes());
|
||||
out.extend_from_slice(&u16::try_from(PPQ).unwrap_or(960).to_be_bytes());
|
||||
|
||||
write_track(&mut out, conductor_events(smf)?)?;
|
||||
for track in &smf.tracks {
|
||||
write_track(&mut out, track_events(track)?)?;
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
+31
-25
@@ -366,34 +366,40 @@ impl AudioState {
|
||||
let b = if i0 + 1 < self.pending_len { ch.and_then(|v| v.get(i0 + 1)).copied().unwrap_or(a) } else { a };
|
||||
a * (1.0 - fr) + b * fr
|
||||
};
|
||||
if channels < nch {
|
||||
// Fold surround to stereo first; a mono device averages the two sides.
|
||||
let (mut l, mut r) = (0.0f32, 0.0f32);
|
||||
if sounding {
|
||||
if nch == 2 {
|
||||
(l, r) = (sample(0), sample(1));
|
||||
} else {
|
||||
for (c, (gl, gr)) in self.fold.iter().enumerate().take(nch) {
|
||||
let v = sample(c);
|
||||
l += v * gl;
|
||||
r += v * gr;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (c, o) in frame.iter_mut().enumerate() {
|
||||
*o = match c {
|
||||
0 if channels == 1 => out_sample(0.5 * l + 0.5 * r),
|
||||
0 => out_sample(l),
|
||||
1 => out_sample(r),
|
||||
_ => 0.0,
|
||||
};
|
||||
}
|
||||
write_frame(frame, channels, nch, sounding, &self.fold, sample);
|
||||
self.read += step;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Write one device frame from `nch` rendered channels: fold surround to stereo (a mono device
|
||||
/// averages the two sides), or copy the channels straight through. `sample(c)` is channel `c`.
|
||||
fn write_frame(frame: &mut [f32], channels: usize, nch: usize, sounding: bool, fold: &[(f32, f32)], sample: impl Fn(usize) -> f32) {
|
||||
if channels < nch {
|
||||
// Fold surround to stereo first; a mono device averages the two sides.
|
||||
let (mut l, mut r) = (0.0f32, 0.0f32);
|
||||
if sounding {
|
||||
if nch == 2 {
|
||||
(l, r) = (sample(0), sample(1));
|
||||
} else {
|
||||
for (c, o) in frame.iter_mut().enumerate() {
|
||||
*o = if c < nch.max(1) && sounding { out_sample(sample(c)) } else { 0.0 };
|
||||
for (c, (gl, gr)) in fold.iter().enumerate().take(nch) {
|
||||
let v = sample(c);
|
||||
l += v * gl;
|
||||
r += v * gr;
|
||||
}
|
||||
}
|
||||
self.read += step;
|
||||
}
|
||||
for (c, o) in frame.iter_mut().enumerate() {
|
||||
*o = match c {
|
||||
0 if channels == 1 => out_sample(0.5 * l + 0.5 * r),
|
||||
0 => out_sample(l),
|
||||
1 => out_sample(r),
|
||||
_ => 0.0,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
for (c, o) in frame.iter_mut().enumerate() {
|
||||
*o = if c < nch.max(1) && sounding { out_sample(sample(c)) } else { 0.0 };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,6 +177,35 @@ impl Vst3Plugin {
|
||||
}
|
||||
}
|
||||
|
||||
/// Copy `n` frames from `start` of the host buffers into the plugin's input `port` (mono sources
|
||||
/// fill every port channel).
|
||||
fn feed_input(inputs: &mut BusBuffers, port: usize, io: &[Vec<f32>], ch: usize, (start, n): (usize, usize)) {
|
||||
for c in 0..inputs.channels(port) {
|
||||
let src = io.get(c.min(ch - 1)).and_then(|s| s.get(start..start + n));
|
||||
if let (Some(src), Some(dst)) = (src, inputs.channel_mut(port, c).and_then(|d| d.get_mut(..n))) {
|
||||
dst.copy_from_slice(src);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Copy `n` frames of the plugin's output `port` into the host buffers from `start`; a mono host
|
||||
/// buffer takes the average of a stereo port.
|
||||
fn read_output(outputs: &BusBuffers, port: usize, io: &mut [Vec<f32>], ch: usize, (start, n): (usize, usize)) {
|
||||
let oc = outputs.channels(port);
|
||||
for (c, dst) in io.iter_mut().take(ch).enumerate() {
|
||||
let Some(dst) = dst.get_mut(start..start + n) else { continue };
|
||||
if ch == 1 && oc >= 2 {
|
||||
if let (Some(l), Some(r)) = (outputs.channel(port, 0), outputs.channel(port, 1)) {
|
||||
for ((d, a), b) in dst.iter_mut().zip(l).zip(r) {
|
||||
*d = 0.5 * (a + b);
|
||||
}
|
||||
}
|
||||
} else if let Some(src) = outputs.channel(port, c.min(oc.saturating_sub(1))).and_then(|s| s.get(..n)) {
|
||||
dst.copy_from_slice(src);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Plugin for Vst3Plugin {
|
||||
fn info(&self) -> &'static PluginInfo {
|
||||
self.info
|
||||
@@ -270,12 +299,7 @@ impl Plugin for Vst3Plugin {
|
||||
let end = u32::try_from(start + n).unwrap_or(u32::MAX);
|
||||
// Feed the main input bus (mono sources fill every bus channel).
|
||||
if self.has_input {
|
||||
for c in 0..self.inputs.channels(0) {
|
||||
let src = io.get(c.min(ch - 1)).and_then(|s| s.get(start..start + n));
|
||||
if let (Some(src), Some(dst)) = (src, self.inputs.channel_mut(0, c).and_then(|d| d.get_mut(..n))) {
|
||||
dst.copy_from_slice(src);
|
||||
}
|
||||
}
|
||||
feed_input(&mut self.inputs, 0, io, ch, (start, n));
|
||||
}
|
||||
let k = self.notes.iter().position(|e| e.time() >= end).unwrap_or(self.notes.len());
|
||||
let offset = u32::try_from(start).unwrap_or(u32::MAX);
|
||||
@@ -296,19 +320,7 @@ impl Plugin for Vst3Plugin {
|
||||
self.position = self.position.saturating_add(i64::try_from(n).unwrap_or(0));
|
||||
match r {
|
||||
Ok(()) if self.has_output => {
|
||||
let oc = self.outputs.channels(0);
|
||||
for (c, dst) in io.iter_mut().take(ch).enumerate() {
|
||||
let Some(dst) = dst.get_mut(start..start + n) else { continue };
|
||||
if ch == 1 && oc >= 2 {
|
||||
if let (Some(l), Some(r)) = (self.outputs.channel(0, 0), self.outputs.channel(0, 1)) {
|
||||
for ((d, a), b) in dst.iter_mut().zip(l).zip(r) {
|
||||
*d = 0.5 * (a + b);
|
||||
}
|
||||
}
|
||||
} else if let Some(src) = self.outputs.channel(0, c.min(oc.saturating_sub(1))).and_then(|s| s.get(..n)) {
|
||||
dst.copy_from_slice(src);
|
||||
}
|
||||
}
|
||||
read_output(&self.outputs, 0, io, ch, (start, n));
|
||||
}
|
||||
Err(e) => log::debug!("{}: {e}", self.info.id),
|
||||
_ => {}
|
||||
|
||||
Reference in New Issue
Block a user