Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,12 @@ yourself and your screen. Nothing extra to install.
- **Scene:** Screen, Bubble (the camera in a round or square corner, small,
medium or large), Side by side, or Camera, the camera mirrored if you like.
The file stays as the camera saw it; the mirror is part of the edit.
Drag the bubble on the preview to move it to another corner.
- **Crop and colour** for the screen and the camera, like OBS's crop and
colour-correction filters: cut any edge away, change brightness,
contrast, saturation and warmth, and key out a green or blue screen behind
you. It shows live, fits every layout, and the take's clips start with it
(the files stay as recorded, so it can be undone or changed later).
- **A file per source**, like OBS's Source Record: every take writes
`screen.mp4`, `camera.mp4`, `microphone.m4a` and `system.m4a` into its own
folder under `~/Movies/Splice/Recordings/<project>/`. All start at the same
Expand All @@ -312,6 +318,8 @@ yourself and your screen. Nothing extra to install.
hiss, hum) lowered, and the level evened out. Switch it on for the mic in
Record, or for any clip under Sound in the inspector; the file stays as
recorded.
- **Mic mode:** with a mic live, the Record tab opens macOS's own menu for
Voice Isolation, Wide Spectrum or Standard, and shows the one in use.
- **Each project keeps its own setup.** "Make this my default" sets the one
new projects start with.
- **Permissions:** Screen Recording, Camera and Microphone, asked for the
Expand Down
14 changes: 14 additions & 0 deletions crates/splice-app/capture/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@ func send(_ kind: UInt8, _ body: Data) {
outLock.unlock()
}

/// The mode macOS runs the microphone in for this app, as chosen in
/// Control Center.
func micMode() -> String {
switch AVCaptureDevice.activeMicrophoneMode {
case .voiceIsolation: return "voice-isolation"
case .wideSpectrum: return "wide-spectrum"
default: return "standard"
}
}

func event(_ name: String, _ fields: [String: Any] = [:]) {
var all = fields
all["event"] = name
Expand Down Expand Up @@ -755,6 +765,7 @@ final class Capture: NSObject, SCStreamOutput, SCStreamDelegate,
var fields: [String: Any] = [:]
for (f, p) in peaks { fields[f.name] = Double(p) }
if zero != nil { fields["seconds"] = seconds() }
if feed == .microphone { fields["mic_mode"] = micMode() }
event("levels", fields)
peaks = [:]
}
Expand Down Expand Up @@ -848,6 +859,9 @@ Thread.detachNewThread {
capture.queue.async { capture.resume() }
case "stop":
capture.queue.async { capture.finish() }
case "micmodes":
// macOS's own menu: Standard, Voice Isolation, Wide Spectrum.
DispatchQueue.main.async { AVCaptureDevice.showSystemUserInterface(.microphoneModes) }
case "quit":
capture.shutdown()
exit(0)
Expand Down
25 changes: 24 additions & 1 deletion crates/splice-app/src/capture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use gpui::RenderImage;
use serde::Deserialize;
use serde_json::{Value, json};
use splice_core::studio::{Codec, Screen, Studio, Take};
use splice_core::tone::Tone;
use std::collections::HashMap;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
Expand Down Expand Up @@ -231,8 +232,14 @@ pub struct Live {
/// The part of the camera's picture the scene shows; its preview frames
/// arrive cut to it, so a bubble can be drawn round.
pub camera_part: Arc<std::sync::Mutex<Option<Part>>>,
/// Each picture's colour and key, worked on its preview frames as
/// they arrive.
pub tones: Arc<std::sync::Mutex<HashMap<Feed, Tone>>>,
/// The loudest each sound has been lately, 0 to 1.
pub levels: HashMap<String, f32>,
/// The microphone's mode in macOS: standard, voice-isolation or
/// wide-spectrum.
pub mic_mode: Option<String>,
/// Sources that have sent something.
pub live: bool,
pub recording: Option<Rec>,
Expand Down Expand Up @@ -269,6 +276,8 @@ impl Live {
let (tx, messages) = channel();
let camera_part = Arc::new(std::sync::Mutex::new(None));
let wanted = camera_part.clone();
let tones: Arc<std::sync::Mutex<HashMap<Feed, Tone>>> = Arc::default();
let toned = tones.clone();
std::thread::Builder::new()
.name("splice-capture".into())
.spawn(move || {
Expand Down Expand Up @@ -296,10 +305,14 @@ impl Live {
Feed::Camera => *wanted.lock().unwrap(),
Feed::Screen => None,
};
let (pixels, size) = match part {
let (mut pixels, size) = match part {
Some(part) => cut(&body, (w, h), part),
None => (body, (w, h)),
};
let tone = toned.lock().unwrap().get(&feed).copied();
if let Some(tone) = tone {
tone.apply(&mut pixels);
}
match ffmpeg::render_image(size.0, size.1, pixels) {
Some(image) => Message::Frame(feed, image, (w, h), part),
None => continue,
Expand All @@ -322,6 +335,8 @@ impl Live {
natural: HashMap::new(),
parts: HashMap::new(),
camera_part,
tones,
mic_mode: None,
levels: HashMap::new(),
live: false,
recording: None,
Expand Down Expand Up @@ -359,6 +374,11 @@ impl Live {
self.command(json!({ "cmd": "pause" }));
}

/// Opens macOS's microphone modes (Voice Isolation and the rest).
pub fn mic_modes(&mut self) {
self.command(json!({ "cmd": "micmodes" }));
}

pub fn resume(&mut self) {
self.command(json!({ "cmd": "resume" }));
}
Expand Down Expand Up @@ -437,6 +457,9 @@ impl Live {
None => self.levels.remove(feed),
};
}
if let Some(mode) = e["mic_mode"].as_str() {
self.mic_mode = Some(mode.to_string());
}
if let (Some(rec), Some(s)) = (&mut self.recording, e["seconds"].as_f64()) {
rec.seconds = s;
}
Expand Down
28 changes: 27 additions & 1 deletion crates/splice-app/src/ctl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,13 @@ Recording (each source to its own file; the take is laid out as the scene says)
--clean/--no-clean (takes' mic clips come in with the voice cleaned up)
--codec h264|hevc --quality good|high|best --fps 30|60 --height full|4k|1440|1080
--cursor | --no-cursor --clicks | --no-clicks (note clicks, to zoom in on later)
record adjust screen|camera [LOOK] [--reset]
change a picture as recorded clips will: --crop-left/-right/-top/
-bottom F, colour (--exposure …), --key green, --blur F, --redact …
(any look option but place and size, which the scene sets); shown
live, and each take's clip starts with it (the file is untouched)
record mic-mode open macOS's Mic Mode menu (Voice Isolation, Wide Spectrum); the
mode in use shows in `record status` as mic_mode
record start [SETUP] [--for S] [--beat B | --at T [--track N] | --into media]
start (answers once recording); the take goes after everything on
the timeline, into beat B, at T, or only into the media
Expand All @@ -216,7 +223,8 @@ Recording (each source to its own file; the take is laid out as the scene says)
record status recording or not, for how long, sound levels, the last take's folder

Operating the window (as a person would: for checking the interface)
ui window size and scale, mouse, stage, timeline and picture bounds
ui window size and scale, mouse, stage, timeline and picture bounds,
and in Record the scene and the camera bubble
click X Y [--double] [--right] [--shift|--cmd|--alt|--ctrl]
hover X Y move the pointer there (tooltips show after a moment)
drag X1 Y1 X2 Y2 [--shift…] press, move, release
Expand Down Expand Up @@ -706,6 +714,10 @@ fn build(args: &[String]) -> Result<Value, String> {
if let Some(action) = words.get(1).filter(|_| sub == "shot" || sub == "take") {
request.insert("action".into(), json!(action));
}
if command == "record" && sub == "adjust" {
let source = words.get(1).ok_or("record adjust expects screen or camera")?;
request.insert("source".into(), json!(source));
}
command.as_str()
}
"view" => {
Expand Down Expand Up @@ -848,6 +860,20 @@ mod tests {
r,
json!({"cmd": "text", "text": "Hi", "size": 96.0, "bold": true, "duration": 2.5})
);
let r = build(&args(
"record adjust camera --crop-left 0.2 --exposure 0.5 --key green --reset",
))
.unwrap();
assert_eq!(
r,
json!({"cmd": "record", "sub": "adjust", "source": "camera", "crop_left": 0.2, "exposure": 0.5,
"key": "green", "reset": true})
);
assert!(build(&args("record adjust")).is_err(), "which picture");
assert_eq!(
build(&args("record mic-mode")).unwrap(),
json!({"cmd": "record", "sub": "mic-mode"})
);
assert!(build(&args("move --clip x")).is_err());
assert!(build(&args("explode")).is_err());
let r = build(&args(
Expand Down
4 changes: 2 additions & 2 deletions crates/splice-app/src/editor/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ pub fn parse_color(text: &str) -> Option<u32> {
}
}

fn look_json(look: &splice_core::look::Look) -> Value {
pub(super) fn look_json(look: &splice_core::look::Look) -> Value {
json!({
"x": look.x,
"y": look.y,
Expand Down Expand Up @@ -1139,7 +1139,7 @@ impl Editor {

/// Look fields from a request, onto `look`.
/// `at` is clip time, where a keyframed value is keyed.
fn apply_look(look: &mut splice_core::look::Look, request: &Value, at: Time) -> Result<(), String> {
pub(super) fn apply_look(look: &mut splice_core::look::Look, request: &Value, at: Time) -> Result<(), String> {
use splice_core::look::{Drift, Key, Mask, MotionKind, Shape};
let text = |key: &str| request.get(key).and_then(Value::as_str);
let number = |key: &str| request.get(key).and_then(Value::as_f64).map(|v| v as f32);
Expand Down
4 changes: 4 additions & 0 deletions crates/splice-app/src/editor/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ impl Editor {
"pictures": layers,
"overlay": !matches!(self.overlay, Overlay::None),
"editor": self.workbench.as_ref().map(|b| b.media.0),
"record": (self.mode == story::Mode::Record).then(|| {
let (scene, bubble) = self.rec.drawn;
json!({ "scene": scene.map(rect), "bubble": bubble.map(rect) })
}),
"note": "Positions are window points: a screenshot's pixels divided by `scale`.",
}));
}
Expand Down
Loading
Loading