This is the sixth of a series of posts about SQA, my pet audio project. I’m rebuilding it from the ground up, for reasons explained here.

I wonder if I should stop calling this a ‘weekly’ publication. I mean, if I had waited a few days more, it’d almost have been a month!

Commit log

  • cdf952b on 2017-03-04: backend: begin work on implementing audio Action
  • 3f54581 on 2017-03-04: backend: change actions API
  • 566104f on 2017-02-17: backend: begin work on actions submodule
  • ae517e7 on 2017-02-17: backend: wrap handler in loop to ensure we only stop when we would block
  • 0eca3c9 on 2017-02-16: backend: add a Remote to ConnData
  • 577d922 on 2017-02-16: backend: generalise Server to Connection, add more functionality

Changes in detail

So…as you may have guessed, implementing the actual backend is a lot harder than just writing library code. In particular, futures are rather hard to comprehend sometimes! This is why it’s taking so long - there’s quite a lot of thinking to be done about how best to implement stuff. I’m learning a lot about Tokio and the mechanics of futures in Rust, though.

This “week” (read: almost month), I’ve been thinking about how to represent the concept of actions in SQA. How programs like QLab (SQA’s main competitor) and SQA work goes basically like this: you program in a timeline of stuff that’s going to happen (actions), and when you want it to happen (on cue? x seconds after something else finishes playing? etc). Basically, I have to figure out how to actually represent the concept of an action in code.

What I’ve settled on thus far:

pub trait ActionController {
    fn desc(&self) -> String;
    fn get_params(&self) -> Vec<Parameter>;
    fn set_param(&mut self, &str, Option<Value>) -> bool;
    fn verify_params(&self, ctx: &mut Context) -> Vec<ParameterError>;
    fn load(&mut self, ctx: &mut Context) -> Result<Option<LoadFuture>, Box<Error>>;
    fn loaded(&mut self, &mut Context, Box<Any>) -> Result<(), Box<Error>>;
    fn execute(&mut self, time: u64, ctx: &mut Context) -> ActionFuture;
    fn duration(&self) -> Option<Duration>;
    fn accept_message(&mut self, Box<Any>) -> Result<(), Box<Error>>;
    fn accept_audio_message(&mut self, msg: &mut AudioThreadMessage) -> bool;
}
pub struct Action {
    ctl: Box<ActionController>,
    state: PlaybackState,
    typ: ActionType,
    uu: Uuid
}

Woah. That’s a lot of methods for one trait. Basically, I’ve represented an action as an ActionController (which is boxed up inside a Box, stored in trait-object form), along with some other stuff that’s common to literally every action out there.

What you do: you call get_params(), which returns a list of parameters for this action (e.g. for an audio file, you’d have something like “starting volume, audio file location, channel output” and so on), you configure parameters with set_param(), and you check that it’s all good with verify_params().

After that, you can start to load() the action (which prepares it for execution) and then execute() it when the time is right.

There are still some problems with this design (error handling being one major one), but I’m working on it.

Once an action is represented, we also actually need ways to do stuff - which means we need to interface with sqa-engine and sqa-ffmpeg to actually play audio. Then, we need to implement a way to manage how the audio comes out (a channel manager), and the necessary commands to actually control all of this over the network.

It’s a lot of work!


That’s it for this “week”.

Every time I say that I’ll publish these weekly, and I always get so bogged down in programming and never get the write-up out the door. I’m going to try and fix this. Let’s see if I actually stick to my promise this time.