diff --git a/Makefile.in b/Makefile.in index b7211a3b..44d0447e 100644 --- a/Makefile.in +++ b/Makefile.in @@ -688,6 +688,18 @@ symstore-tarball: $(SYMSTORE_OBJ64)/symstore --skip-managed $(DST_BASE) $(OBJ)/symstore/$(BUILD_NAME) cd $(OBJ)/symstore/$(BUILD_NAME) && zip -r ../$(BUILD_NAME)-symstore.zip . >& /dev/null +## +## Media +## + +DIST_MEDIA := $(DST_DIR)/share/media +$(DIST_MEDIA): + mkdir -p $@ + cp $(SRCDIR)/media/blank.mkv $@ + cp $(SRCDIR)/media/blank.ptna $@ + +all-dist: $(DIST_MEDIA) + ## ## Fonts ## diff --git a/media/Makefile b/media/Makefile new file mode 100644 index 00000000..4e7d439f --- /dev/null +++ b/media/Makefile @@ -0,0 +1,12 @@ +all: blank.mkv blank.ptna + +blank.mkv: + #120 frames @ 30 FPS == 4 seconds + gst-launch-1.0 videotestsrc num-buffers=120 pattern=smpte ! 'video/x-raw,format=(string)I420,width=320,height=240,framerate=(fraction)30/1' ! av1enc ! queue ! mux. audiotestsrc num-buffers=400 freq=0 samplesperbuffer=441 ! 'audio/x-raw,rate=48000,channels=2' ! opusenc ! queue ! mux. matroskamux name=mux ! filesink location=blank.mkv + +make_blank_ptna: make_blank_ptna.c + gcc -Wall -O2 $(shell pkg-config --cflags opus) -o $@ $< -lm $(shell pkg-config --libs opus) + +blank.ptna: make_blank_ptna + ./make_blank_ptna $@ + diff --git a/media/blank.mkv b/media/blank.mkv new file mode 100644 index 00000000..7a05753b Binary files /dev/null and b/media/blank.mkv differ diff --git a/media/blank.ptna b/media/blank.ptna new file mode 100644 index 00000000..44af1343 Binary files /dev/null and b/media/blank.ptna differ diff --git a/media/make_blank_ptna.c b/media/make_blank_ptna.c new file mode 100644 index 00000000..ace3de7f --- /dev/null +++ b/media/make_blank_ptna.c @@ -0,0 +1,271 @@ +/* + * Copyright (c) 2020, Valve Corporation + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* Dumps a "blank" Proton Audio sample. For documentation of the Proton Audio + * format, see audioconv.rs. */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define ARRAY_SIZE(x) (sizeof(x) / sizeof(*x)) + +/* indexes into frame_sizes[] */ +#define FRAME_2p5MS 0 +#define FRAME_SMALLEST FRAME_2p5MS +#define FRAME_5MS 1 +#define FRAME_10MS 2 +#define FRAME_20MS 3 +#define FRAME_40MS 4 +#define FRAME_60MS 5 +#define FRAME_LARGEST FRAME_60MS + +/* sample properties */ +static const int SAMPLERATE = 48000; +static const int CHANNELS = 1; +static const int SAMPLE_LENGTH = FRAME_10MS; /* must match value in audioconv.rs */ +static const float AUDIBLE_HZ = 400.f; /* freq for the --audible switch */ + +static const int frame_sizes[] = { + SAMPLERATE * 2.5 / 1000, + SAMPLERATE * 5 / 1000, + SAMPLERATE * 10 / 1000, + SAMPLERATE * 20 / 1000, + SAMPLERATE * 40 / 1000, + SAMPLERATE * 60 / 1000, +}; + +static float theta = 0.f; + +static const uint32_t FLAG_HEADER = 0x10000000; +static const uint32_t AUDIOCONV_PADDING_LENGTH_SHIFT = 12; + +static int complete_write(const int file, const void *buf, const size_t len) +{ + size_t written = 0; + ssize_t ret; + while(written < len){ + ret = write(file, ((const char *)buf) + written, len - written); + if(ret < 0){ + if(errno != EINTR && errno != EAGAIN) + return 0; + }else + written += ret; + } + return written == len; +} + +static int dump_hz(float hz, int samples, OpusEncoder *encoder, int outfile) +{ + int i = 0, j, c, frame_size, tot = 0, padding; + opus_int32 written; + uint32_t pkt_header; + float val, *vals; + unsigned char packet[4000 /* suggested by opus_encoder(3) */ ]; + + fprintf(stdout, "dumping %u samples at %f hz\n", samples, hz); + + vals = malloc(sizeof(float) * CHANNELS * frame_sizes[FRAME_LARGEST]); + + while(i < samples){ + + /* find the largest packet we can write with the samples remaining */ + frame_size = -1; + for(j = ARRAY_SIZE(frame_sizes) - 1; j >= 0; --j){ + if(samples - i >= frame_sizes[j]){ + frame_size = frame_sizes[j]; + break; + } + } + + if(frame_size < 0){ + /* couldn't fill a whole packet, so write a partial */ + frame_size = frame_sizes[FRAME_SMALLEST]; + padding = frame_size - (samples - i); + }else + padding = 0; + + for(j = 0; j < frame_size - padding; ++j){ + val = sinf(theta); + for(c = 0; c < CHANNELS; ++c) + vals[j * CHANNELS + c] = val; + theta += hz * 2.f * M_PI / (float)SAMPLERATE; + while(theta >= 2.f * M_PI) + theta -= 2.f * M_PI; + } + + if(padding) + memset(vals + (frame_size - padding) * CHANNELS, 0, sizeof(float) * padding * CHANNELS); + + written = opus_encode_float(encoder, vals, frame_size, packet, sizeof(packet)); + if(written < 0){ + fprintf(stderr, "fatal: opus_encode failed!!!\n"); + free(vals); + return 0; + } + + pkt_header = written | (padding << AUDIOCONV_PADDING_LENGTH_SHIFT); + + fprintf(stdout, "encoded %u samples (%u are padding) to %u bytes\n", frame_size, padding, written); + if(!complete_write(outfile, &pkt_header, sizeof(pkt_header))){ + fprintf(stderr, "fatal: error writing packet header!!!\n"); + free(vals); + return 0; + } + if(!complete_write(outfile, packet, written)){ + fprintf(stderr, "fatal: error writing packet contents!!!\n"); + free(vals); + return 0; + } + tot += written + sizeof(written); + + i += frame_size; + } + + fprintf(stdout, "done dumping, %u bytes\n", tot); + + free(vals); + + return 1; +} + +/* OGG's opus header (from RFC 7845 Section 5.1) */ +struct __attribute__((__packed__)) opus_header { + uint64_t magic; + uint8_t version; + uint8_t channels; + uint16_t preskip; + uint32_t input_samplerate; + uint16_t output_gain; + uint8_t mapping_family; +}; + +static int dump_header(int outfile) +{ + struct opus_header header; + uint32_t sz = sizeof(header) | FLAG_HEADER; + + static const char magic[] = "OpusHead"; + + memcpy(&header.magic, magic, sizeof(magic)); + header.version = 1; + header.channels = CHANNELS; + header.preskip = 0; + header.input_samplerate = SAMPLERATE; + header.output_gain = 0; + header.mapping_family = 0; /* FIXME: we need to support mc */ + + if(!complete_write(outfile, &sz, sizeof(sz))){ + fprintf(stderr, "fatal: error writing opus header header!!!\n"); + return 0; + } + + if(!complete_write(outfile, &header, sizeof(header))){ + fprintf(stderr, "fatal: error writing opus header!!!\n"); + return 0; + } + + return 1; +} + +void usage(const char *name) +{ + fprintf(stderr, "usage:\n"); + fprintf(stderr, "\t%s [--help|-h] [--audible] \n", name); + fprintf(stderr, "\n"); + fprintf(stderr, "\t--audible\tGenerate an audible sound clip instead of silence.\n"); + fprintf(stderr, "\t--help -h\tPrint this help message.\n\n"); +} + +int main(int argc, char **argv) +{ + int err, outfile, i, audible = 0; + OpusEncoder *encoder; + const char *outfile_name = NULL; + + for(i = 1; i < argc; ++i){ + if(!strcmp(argv[i], "--help") || !strcmp(argv[i], "-h")){ + usage(argv[0]); + return 0; + }else if(!strcmp(argv[i], "--audible")){ + audible = 1; + }else if(outfile_name){ + fprintf(stderr, "error: too many arguments.\n\n"); + usage(argv[0]); + return 1; + }else{ + outfile_name = argv[i]; + } + } + + if(!outfile_name){ + fprintf(stderr, "error: missing filename.\n\n"); + usage(argv[0]); + return 1; + } + + encoder = opus_encoder_create(SAMPLERATE, CHANNELS, OPUS_APPLICATION_AUDIO, &err); + if(!encoder){ + fprintf(stderr, "couldn't create opus encoder, err: 0x%x\n", err); + return 1; + } + + outfile = open(outfile_name, O_CREAT | O_WRONLY | O_TRUNC, 0644); + if(outfile < 0){ + fprintf(stderr, "couldn't open file \"%s\": %s\n", outfile_name, strerror(errno)); + opus_encoder_destroy(encoder); + return 1; + } + + if(!dump_header(outfile)){ + close(outfile); + unlink(outfile_name); + opus_encoder_destroy(encoder); + return 1; + } + + if(!dump_hz(audible ? AUDIBLE_HZ : 0.f, frame_sizes[SAMPLE_LENGTH], encoder, outfile)){ + close(outfile); + unlink(outfile_name); + opus_encoder_destroy(encoder); + return 1; + } + + close(outfile); + opus_encoder_destroy(encoder); + + return 0; +} diff --git a/proton b/proton index 34101b35..36b9c4bf 100755 --- a/proton +++ b/proton @@ -415,6 +415,7 @@ class Proton: self.lib_dir = self.path("files/lib/") self.lib64_dir = self.path("files/lib64/") self.fonts_dir = self.path("files/share/fonts/") + self.media_dir = self.path("files/share/media/") self.wine_fonts_dir = self.path("files/share/wine/fonts/") self.wine_inf = self.path("files/share/wine/wine.inf") self.version_file = self.path("version") @@ -1258,6 +1259,9 @@ class Session: self.env["MEDIACONV_AUDIO_TRANSCODED_FILE"] = os.environ["STEAM_COMPAT_TRANSCODED_MEDIA_PATH"] + "/transcoded_audio.foz" self.env["MEDIACONV_VIDEO_TRANSCODED_FILE"] = os.environ["STEAM_COMPAT_TRANSCODED_MEDIA_PATH"] + "/transcoded_video.foz" + self.env["MEDIACONV_BLANK_VIDEO_FILE"] = g_proton.media_dir + "blank.mkv" + self.env["MEDIACONV_BLANK_AUDIO_FILE"] = g_proton.media_dir + "blank.ptna" + prepend_to_env_str(self.env, "PATH", g_proton.bin_dir, ":") def check_environment(self, env_name, config_name):