Compare commits

...

735 commits

Author SHA1 Message Date
Christopher Snowhill
bafbc511c6 Emergency Bug Fix: Fix loading individual tracks
And folders of tracks, too! The only thing that worked in 3117 was
playlist files. Hah!

Fixes #436

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-05-11 05:32:49 -07:00
Christopher Snowhill
70042c39f8 Sentry: Update sentry-cocoa to version 8.50.1
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-05-10 00:40:36 -07:00
Christopher Snowhill
1eddc831f3 VGMStream: Updated libvgmstream code base
Updated VGMStream to r1980-277-g72cb4b89

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-05-09 21:13:13 -07:00
Christopher Snowhill
dd69345f54 Track loading: Change file import and sorting
Files are now loaded to unique keys, and containers such as playlists
and CUE sheets maintain their file order. Deduplication now only applies
to top level files and not playlist contents. Sorting applies to top
level files, and playlist or container names, but not their contents.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-05-09 21:04:35 -07:00
Christopher Snowhill
27b1157359 VGMStream: Updated libvgmstream code base
Updated VGMStream to r1980-268-gc32951e9

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-05-03 01:56:03 -07:00
Christopher Snowhill
fcec44982c Converter Node: Change volume scale observer
This should fix an exception being thrown because the observer wasn't
registered, or known to be registered. Only register it when it will be
used, and only unregister it if it was registered.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-05-03 01:23:19 -07:00
Christopher Snowhill
1bea1699a1 HDCD: Stop dynamically halving the volume
Apparently, this doesn't work too well with real HDCD tracks, and causes
all sorts of weird volume issues. Just leave the volume alone, and let
HDCD decoding make tracks louder, possibly, rather than try to halve the
volume automatically on a detector that isn't terribly good.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-04-24 14:54:41 -07:00
Christopher Snowhill
bd8aa0e91b HDCD: Fix how unsigned audio may be processed
Unsigned will alter the input, so move it like the other integer sample
processors do.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-04-24 14:53:18 -07:00
Christopher Snowhill
46ee7fc8b9 HDCD: Make HDCD extension processing optional
And disabled by default, at that. I can't actually hear the difference
of Peak Extension in the Rock track I have that claims to use it. And
Low Level Range Extension is more trouble than it's worth on tracks that
use it by mistake, or maliciously, if the case may be. I may add track
tag level control in the future.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-04-23 21:43:51 -07:00
Christopher Snowhill
62bf2452f2 Maintenance: Touched by Xcode
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-04-23 21:42:01 -07:00
Christopher Snowhill
d2e81b7de5 VGMStream: Fix memory leak
Thanks to upstream VGMStream for fixing this in the Audacious plugin,
which I derived this code from in the first place, which explains the
memory leak getting in.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-04-20 21:34:57 -07:00
Christopher Snowhill
3408c0f791 VGMStream: Add native support for 24/32 bps int
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-04-20 21:34:11 -07:00
Christopher Snowhill
531c2b1468 VGMStream: Updated libvgmstream code base
Updated VGMStream to r1980-242-gfccbb05f

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-04-20 21:33:11 -07:00
Christopher Snowhill
baf8d1e745 Updated Sparkle to version 2.7.0
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-04-20 21:03:11 -07:00
Christopher Snowhill
07fed67f18 Updated MASShortcut to fix crashes
I hope this works to fix the crashes from migration.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-04-20 20:59:53 -07:00
Christopher Snowhill
6061901218 Path Suggester: Make font fixed size
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-04-07 14:13:15 -07:00
Christopher Snowhill
388e916f2a Path Suggester: Touched by Xcode
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-04-07 14:12:55 -07:00
Christopher Snowhill
a9ce4f4eff Bug Fix: Fix FFmpeg reporting timestamps for DSD
Raw DSD is counting frames in bytes, not bits/samples, so it needs to be
scaled up when dividing by the raw sample rate.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-28 20:26:27 -07:00
Christopher Snowhill
e9b15bc269 MIDI: Add unstable configuration for AU players
It's regarded as unstable as some plugins randomly decide to overflow
the main thread queue and crash the player on the main thread check, but
only sometimes, and not always.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-28 16:18:04 -07:00
Christopher Snowhill
56dcf4df86 Sandbox Security: Disable library validation
Apparently, this was preventing some signed plugins from being loaded
into the process, preventing their use as MIDI plugins. And this may be
needed for future AudioUnit DSP filter support, for third party plugins
there, as well.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-28 16:18:00 -07:00
Christopher Snowhill
9480bc742c MIDI: Move sample buffer from stack to class
Move the stack-based buffer, which is rather large, to the player class
instance, where it will be allocated on the heap.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-28 16:17:56 -07:00
Christopher Snowhill
d590a03595 MIDI: Fix Audio Unit player in several ways
Improve rendering functions of Audio Unit player, and also fix looping
for the Audio Unit player, and any other possible future players which
use blocked decoding with timestamped events.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-28 16:17:51 -07:00
Christopher Snowhill
2565767be2 MIDI: Make BASSMIDI the visible default
Also hide the Apple plugins from settings so they don't get activated
unless there's no SoundFont configured.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-28 16:17:43 -07:00
Christopher Snowhill
a708851b63 Dependencies: Update BASS and WavPack libraries
BASSMIDI: 2.4.15.3
BASSFLAC: 2.4.5.5
BASSOPUS: 2.4.3
BASSWV: 2.4.7.4

WavPack: 5.8.1

And updated the WavPack plugin to support threaded decoding, using up to
four worker threads, as detected from the host machine's CPU count.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-28 16:17:25 -07:00
Christopher Snowhill
c4a5c8f45b MIDI: Update BASS docs
This was the version of BASSWV previously bundled.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-27 18:45:00 -07:00
Christopher Snowhill
c5732aa13b Audio Output: Optimize fader function
Now using Accelerate methods to calculate and multiply ramps per channel
and add the remainder if necessary.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-27 17:13:38 -07:00
Christopher Snowhill
355bdf8616 Play Control: Previous track now also restarts
If the current track has played for more than 5 seconds, previous track
now restarts the current track, instead of jumping back to the previous
track.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-26 20:09:47 -07:00
Christopher Snowhill
954cfa02d9 Bug Fix: Fix inserting empty chunks on track ends
This code did not check the number of samples in a packet before adding
it to the output buffer, which apparently had the potential to cause the
output code to emit up to 512 samples of silence between tracks. This,
as one can guess, is a bad thing, and causes noticeable gapping between
tracks.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-26 19:07:36 -07:00
Christopher Snowhill
576b199382 Bug Fix: Fix output logging, switch log method
Output logging, a debugging feature that is only enabled at build time
if I need to chase down some audio mixing or output bug, was not logging
anything at all. Change to use Cocoa file writing methods, and actually
implement the output writer function again.

This code is left disabled 99% of the time anyway, and especially in
release builds. Like the node logging code elsewhere, it has the
potential to be very noisy and consume massive amounts of disk space.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-26 19:06:02 -07:00
Christopher Snowhill
d1ff9ba0c0 Bug Fix: Include soxr latency in memory allocation
This should be included, for safety purposes, in case the rounding up to
the nearest multiple of 256 samples doesn't bump the buffer size enough.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-26 19:03:26 -07:00
Christopher Snowhill
8f595af704 Bug Fix: Retry MP3 file a few times before failure
Give up after 10 tries.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-24 16:05:19 -07:00
Christopher Snowhill
f73cde09ee Bug Fix: Fix minimp3 to deal with invalid files
If a file can't decode, there should not be a division by zero error.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-24 15:42:30 -07:00
Christopher Snowhill
2c4ca7d51d Bug Fix: Fix minimp3 streaming support
The streaming support was breaking because initial packet detection was
failing due to bit reservoir errors. Instead, detect consecutive sync
frames in the initial read buffer, then attempt to sync to a decodable
frame in the first block of data, otherwise give up.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-24 08:16:37 -07:00
Christopher Snowhill
0725f10be6 FFmpeg: Fix HLS, HLS metadata, update FFmpeg
Add missing HLS MIME type: audio/mpegurl

Update FFmpeg to version 7.1.1, carrying the same patches, and one new
patch: Implementing support for HLS ID3 tags changing mid-stream.

We cannot do away with fdk-aac yet, because the USAC codec is missing
features that fdk-aac implements already.

Fixes #428

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-24 06:08:22 -07:00
Christopher Snowhill
2ef4594916 Bug Fix: Fix minimp3 seek position
Seek offset is scaled by the number of channels in the file. Oops again.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-23 20:22:39 -07:00
Christopher Snowhill
fc2a14b0fa Bug Fix: Free memory buffers used by minimp3
This was being leaked when playing static, seekable files. Oops.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-23 20:03:44 -07:00
Christopher Snowhill
410395dded minimp3: Numerous buffering fixes, consolidation
Move minimp3 packet decoder state into the decoder_ex state structure,
instead of using the redundant duplicate structure. Also reduce input
buffer size for streams to 16KiB, and actually use the defined macro in
the header file to declare the streaming buffer size.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-23 20:02:58 -07:00
Christopher Snowhill
6e8538c7c9 minimp3: Fix seeking behavior
Seeking should clear the sample buffer if it contains anything, and non-
seekable files should return an error on an attempt to seek.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-23 16:18:07 -07:00
Christopher Snowhill
11eeaea6e2 VGMStream: Update associated filename extensions
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-23 16:15:21 -07:00
Christopher Snowhill
2755ce5e64 VGMStream: Considerably rewrite plugin interface
It was about time to rewrite this anyway. Now adapted to the new public
interface API.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-23 16:13:24 -07:00
Christopher Snowhill
7bc011ddee Bug Fix: Fixed several code signing issues
There were several issues which broke debug and possibly release
signing, and broke VGMStream in debug situations. Not sure if it was
also broken in release.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-23 15:47:36 -07:00
Christopher Snowhill
c6bca5aa39 VGMStream: Updated libvgmstream code base
Updated VGMStream to r1980-181-g10093db5

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-23 15:45:54 -07:00
Christopher Snowhill
c8ce0f3f81 Bug Fix: Reorder LPC scratch memory for alignment
The double members should be ordered first so they are aligned to an 8
byte boundary. The rest are fine as-is.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-23 00:04:40 -07:00
Christopher Snowhill
3a62776fa6 MP3: Replace MAD with minimp3
libMAD had memory safety issues, possibly with corrupt files. Hopefully,
this will fix the problem.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-22 23:59:11 -07:00
Christopher Snowhill
e7779278bd Code Fix: Change visualizers to only copy FFT data
These two visualization components, SceneKit and Core Graphics based,
only use the FFT data. So now make the request drop the PCM data.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-13 19:50:35 -07:00
Christopher Snowhill
ff66c8e1a9 Code Fix: Add nullability flags to Vis Manager
The Visualization Manager PCM/FFT copy function was already observing
these parameters for null input and only returning output to the ones
which were not null. This just makes it clear that they are both
optional parameters. This is useful for future visualization adventures,
allowing PCM copy without invoking the FFT processing, or requesting FFT
without also having to keep the PCM.

This is mostly only a compile time change, and has no noticeable effect
on the current runtime behavior, as the only consumers of the Visualizer
data currently request both PCM and FFT.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-13 19:47:35 -07:00
Christopher Snowhill
fb97fbd202 Sentry: Update sentry-cocoa to version 8.47.0
This release is a bug fix against the Sentry integration from the
upstream project.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-13 19:44:40 -07:00
Christopher Snowhill
9aaf6d1c2d Crash Fix: Only selectively register observer
This affects User Defaults, but only has any effect on ChunkLists which
are being used for conversion, and only if they're processing DSD source
material. Thus, the observer should only be added on the one stream that
is converting DSD, and should definitely be removed when the object is
deallocated.

This fixes a serious crash bug that mostly appears to only affect Intel
Macs, and has no major side effects on Apple Silicon that I can tell.
It's a good thing I still own an Intel Mac or two to test on, even if
they are both trapped on older releases of macOS.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-13 19:43:36 -07:00
Christopher Snowhill
fdd0244067 Bug Fix: Track advancing when Rubber Band disabled
Apparently I somehow didn't notice this situation because I still had
Rubber Band enabled, and existing users kept it enabled ever since I
introduced it.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-11 14:29:49 -07:00
Christopher Snowhill
15eaa877b1 Core Audio: Implement proper fade on seek
Whew, what a mess! And this may pave the way for crossfading.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-10 23:08:49 -07:00
Christopher Snowhill
9b973a4b53 Bug Fix: Don't display notification on seek
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-10 23:03:47 -07:00
Christopher Snowhill
691d07ad5a Metadata: Move encoding helper to CogAudio
Move this commonly used string decoding helper to the CogAudio framework
and import it in every plugin that uses it.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-10 14:55:41 -07:00
Christopher Snowhill
2b52d2a766 Bug Fix: Handle invalid UTF-8 decoding errors
Apparently, stringWithUTF8String: just returns nil when the encoding is
not UTF-8, rather than throwing an exception.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-10 14:39:38 -07:00
Christopher Snowhill
845b33e422 App Store: Add encryption attestation
Finally add this to Info.plist properties to save a few seconds on
submissions.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-09 23:50:24 -07:00
Christopher Snowhill
cdc35c7cae Bug Fix: Change how pause stops unseekable files
Change the stop action slightly.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-09 23:45:19 -07:00
Christopher Snowhill
b34e1b5c6d Bug Fix: Disable seeking hotkeys when unseekable
Disable the seek forward and backward actions when the current track
doesn't support seeking.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-09 23:45:11 -07:00
Christopher Snowhill
60ef6c873c Preferences: Touched by Xcode
Xcode really wants to add focusRingType="none" to everything now upon
opening with the editor, for some reason.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-09 23:19:09 -07:00
Christopher Snowhill
a192ccf875 Bug Fix: Add more constraints to general prefs
Add more constraints so the checkboxes are properly spaced again.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-09 23:13:10 -07:00
Christopher Snowhill
13f67cf0f4 Playlist Loader: Add option to skip playlists
When parsing folders, add option to skip importing playlist files.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-09 23:01:40 -07:00
Christopher Snowhill
bd0358b3c6 Sync with main branch
This empty commit pairs up with a XIB fix to the main branch.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-09 22:31:32 -07:00
Christopher Snowhill
2ddbdb953d FFmpeg: Handle multiple attached pictures properly
Now it handles multiple attached pictures and tries to pick out the one
which may be the front cover picture, or otherwise picks the first one.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-09 20:06:41 -07:00
Christopher Snowhill
dbee64e755 Crash Fix: Fix a serious bug in previous commit
Fixes commit 3874d65ec2, where code was
treating a dictionary like an array.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-09 18:30:06 -07:00
Christopher Snowhill
a3268e6a95 Bug Fix: Fix circular bind setter loop in hotkeys
MASShortcut had a potential circular loop in its bindings, let's fix
that right up.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-09 18:22:48 -07:00
Christopher Snowhill
3874d65ec2 Bug Fix: Operation blocks should queue inputs
Operation blocks cannot expect their out of scope variables to be
present when the block executes, so design the block operation to pull
inputs from a queue array one at a time, like the rest of the blocks do.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-09 15:05:03 -07:00
Christopher Snowhill
14a8a35dac Bug Fix: Handle possible null exceptions
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-09 15:03:31 -07:00
Christopher Snowhill
4c073efbfd Core Audio: Fix pausing glitches
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-09 14:43:06 -07:00
Christopher Snowhill
05f2434462 Disable global hotkeys again for macOS < 15.0
Clearly, NSUserDefaults bindings were not really meant to be used in
practice at all, as they do nothing but crash the app.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-09 14:26:33 -07:00
Christopher Snowhill
6036000214 Re-enable hotkeys and change preferences storage
Change hotkey storage system, hopefully this will fix the stability
issues that have been plaguing it for a while now.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-08 20:21:39 -08:00
Christopher Snowhill
e9df18c067 Bug Fix: Disable hotkeys on macOS older than 15.0
Apparently even touching the NSUserDefaults with MASShortcut there is
crash inducing.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-08 12:34:25 -08:00
Christopher Snowhill
3f4e35ed17 Bug Fix: Actually perform a fade in
It doesn't fade if we don't advance the sample pointer. Ugh.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-08 00:38:38 -08:00
Christopher Snowhill
3dbde22f61 Core Audio: Slight change to audio fade in on seek
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-08 00:38:00 -08:00
Christopher Snowhill
653d143c03 Core Audio: Increase fade duration to 125ms
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-07 23:45:02 -08:00
Christopher Snowhill
b08de34bf0 Core Audio: Shut off device after fade out
And resume playback before fade in.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-07 23:44:55 -08:00
Christopher Snowhill
5e48382774 Core Audio: Add a slight fading to operations
Add 10 millisecond fade to seeking, pausing and unpausing, and stopping
on command.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-07 23:22:16 -08:00
Christopher Snowhill
fab4d3705e Bug Fix: Prevent hangs when starting paused
This happens when the player is resumed paused, sometimes near the end
of a track.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-07 20:14:29 -08:00
Christopher Snowhill
fd57ed12bb Bug Fix: Disable hotkeys configuration on macOS<15
MASShortcutView is apparently buggy on older macOS versions. So everyone
there gets hard coded shortcuts and nothing else.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-07 17:49:47 -08:00
Christopher Snowhill
c89e7396cd Bug Fix: Simplification of chunk duration check
This only needs to check that the chunk is empty, not its exact
duration.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-07 17:27:54 -08:00
Christopher Snowhill
216cc52719 Bug Fix: Correct playback of DSD formats
DSD formats were buffering incorrectly and terminating way too soon.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-07 17:26:58 -08:00
Christopher Snowhill
267948350a Bug Fix: Restart converter on format change
The converter doesn't just require an output format call, it also
requires this input format change callback to actually signal it to
reopen the converter process with a new format setup.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-07 05:38:59 -08:00
Christopher Snowhill
780d9ae759 Bug Fix: Ensure robust output format changes
Output format mostly requires stopping and restarting the output device,
and this also prevents us from using the latency function properly,
which apparently always returns 0 for output devices anyway. These
changes also prevent the output callback from hanging when resets occur.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-07 05:37:04 -08:00
Christopher Snowhill
694de375ea Crash Fix: Change how default shortcuts are stored
It turns out that initializing NSUserDefaultsController like this is a
really bad idea, especially on older versions of macOS. This is probably
also why the equalizer was crashing for people on first activation.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-07 04:39:44 -08:00
Christopher Snowhill
d19c2f1e95 Cleanup: Remove unused code
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-07 04:38:04 -08:00
Christopher Snowhill
d25d84c2be Quality of Life: Make buildable with old Xcode
Make the code mostly buildable with Xcode as old as 13.2.1, for debug
testing on Big Sur.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-07 04:37:32 -08:00
Christopher Snowhill
9a4695a80d Translation: Missing string recently added
Translation provided.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-06 16:47:08 -08:00
Christopher Snowhill
b7f6c1c562 Sentry: Replace deprecated SentryUserFeedback use
Replace with newer SentryFeedback.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-06 14:58:04 -08:00
Christopher Snowhill
fccaf31e16 Sentry: Temporarily disable app hang detection
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-06 14:51:49 -08:00
Christopher Snowhill
5eaa4c7774 Translation: Properly support this string
Properly support translating the System Default Device name for sound
output devices. Pending a Spanish translation, but in its current state,
it's no different from where it was before this change.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-06 14:44:11 -08:00
Christopher Snowhill
6eaa4b28c2 Bug Fix: Default output device changes monitoring
Fix default output device logging, and also the preferences if no
default device happens to be set.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-06 14:42:10 -08:00
Christopher Snowhill
6c3af4b2a4 Bug Fix: Latency reporting for high latency output
Fixes visualization latency under virtual machines, at least. Not sure
which local or native systems would be reporting high latency here, but
this should fix them as well.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-06 13:14:27 -08:00
Christopher Snowhill
143010f91c Major Bug Fix: Volume control works again
Shouldn't have broken this again.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-06 10:17:48 -08:00
Christopher Snowhill
40c4f1b780 Feature: Add seeking hotkeys with defaults
Defaulting to ctrl+command+left/right arrows.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-06 10:16:59 -08:00
Christopher Snowhill
c48b74d52d Bug Fix: Move default hotkeys to main app startup
Instead of the Preferences plugin.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-06 03:17:19 -08:00
Christopher Snowhill
0b1482b3c6 Sound Output: Move DSPs, restructure output buffer
Move the DSPs to the output node, so they don't get closed and reopened
across each file. Also restructure the output handler to buffer a little
on its own, to account for track switch activity.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-05 20:05:33 -08:00
Christopher Snowhill
fcb2639d01 Sentry: Bump to version 8.46.0
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-05 15:26:03 -08:00
Christopher Snowhill
00e18da683 Bug Fix: Add more guards to sound output block
These guards should prevent the one crash we saw logged.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-05 15:24:54 -08:00
Christopher Snowhill
3d574ba187 Bug Fix: Correct exception handling blocks
These should be catching NSException*, not generic `id`.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-05 15:24:10 -08:00
Christopher Snowhill
93bec8ca63 Bug Fix: Handle compile time warning
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-04 00:45:53 -08:00
Christopher Snowhill
a7e65d3a85 Bug Fix: Handle gaplessness for headphone filter
The filter uses a pre-buffer of input audio, so extrapolate from the
actual input to fill the buffer. Fixes clicking on non-zero-crossing
track endings.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-04 00:45:31 -08:00
Christopher Snowhill
019bdd7a36 Bug Fix: Restructure Rubber Band gapless handler
Change how samples are accounted for by the filter.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-04 00:44:59 -08:00
Christopher Snowhill
bce00aff2e Bug Fix: Greatly improve audio buffer handling
Buffers were being treated as empty before they were actually processed,
due to races between the current node's end of stream marker and
actually feeding the output buffer.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-04 00:15:47 -08:00
Christopher Snowhill
915e212ae5 Bug Fix: Snap pitch and tempo settings to 1
Pitch and tempo weren't snapping to exactly 1.0 before, as a result of
various things. This fixes that.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-04 00:12:44 -08:00
Christopher Snowhill
001f3e53ea Debugging: Implement buffer chain logging code
This optional code, disabled at compile time by default, allows finding
weird issues with the sample decoding chain.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-03 22:24:17 -08:00
Christopher Snowhill
b631cf803e Improvement: Hopefully improve tag loading speed
Hopefully this works for most ASCII and UTF-8 tags, and continues to
work for weird tag encodings.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-03 22:22:02 -08:00
Christopher Snowhill
b4b0deebd2 Bug Fix: Fix .gitignore file
Oops, the "build" folder reference was incorrect.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-03 22:21:19 -08:00
Christopher Snowhill
81dac451b2 CI: Bump OS and Xcode versions
Bump to macOS 15 and Xcode 16.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-03 19:01:22 -08:00
Christopher Snowhill
7719ccf864 Playback: Implement Selection Follows Playback
Option to make selection follow the playback, within the lag of the
output buffer, including if Always Stop After Current or Repeat One is
enabled. Allows easily queueing up a list of tracks in Always Stop mode,
then hitting the Play button again to play the next track. Enabled by
default, but always optional to disable.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-03 18:34:40 -08:00
Christopher Snowhill
8d848bc745 Playback: Implement Always Stop After Current
A new menu option under the Control menu, disabled by default, which
stops playback after the current track completes.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-03 18:32:01 -08:00
Christopher Snowhill
cee604c63c Miscellaneous: File touched by Xcode
Xcode sure does love to tweak the numbers randomly.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-03 18:28:42 -08:00
Christopher Snowhill
68076ec855 Bug Fix: Wait for output to shut down first
In case stop function called on another thread, wait for it to complete
first.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-01 15:18:10 -08:00
Christopher Snowhill
62010394ef Bug Fix: Remove observer cleanup
Apparently, this isn't needed, and on two users reporting crashes,
actually causes exceptions to be thrown somewhere.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-03-01 15:17:30 -08:00
Christopher Snowhill
b8580cf193 Seeking: Restart output completely on track seek
This required some minor workarounds to deal with the play time counting
that works toward play count reporting.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-28 17:56:10 -08:00
Christopher Snowhill
07a36873e3 Bug Fix: Attempt to solve a next track crash
This was attempting to retrieve the NSURL host object, possibly on a
file URL where the two did not match a previous check. Now we only pass
the full scheme/host/path check if both URLs are not file URLs.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-28 16:33:50 -08:00
Christopher Snowhill
ba52c69a5a Optimization: Perform container checks in queue
Perform the file container checks in an operation queue, since those are
a major bottleneck at this point, too.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-27 19:05:07 -08:00
Christopher Snowhill
6f269dd689 Bug Fix: Unregister observer correctly
Only unregister it if it was actually registered.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-27 14:39:10 -08:00
Christopher Snowhill
970f472436 Cleanup: Remove unused code
This is no longer needed.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-27 14:31:44 -08:00
Christopher Snowhill
d253a59ee6 Bug Fix: Correctly set audio volume on play start
Play start was missing this somehow, after a specific commit removed
some code.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-27 14:31:07 -08:00
Christopher Snowhill
631e8a2c23 Feature: Add fractional track length tooltips
Add fractional track length tooltips, for extra verbose info goodness.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-27 00:58:18 -08:00
Christopher Snowhill
c1b888a21c Bug Fix: Playlist Loader now correctly sorts items
Playlist Loader was sorting only the non-container tracks, and not the
final track list. Move the sort operation to the end of the processing.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-27 00:56:58 -08:00
Christopher Snowhill
55d738cbe8 Cleanup: Remove unused variable
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-27 00:55:43 -08:00
Christopher Snowhill
e1a3e3d2dc Bug Fix: Rubber Band should now flush last chunk
There is a race condition with the next Node in the chain and the End of
Stream marker, considering how tiny the buffering is for these DSPs. Set
End of Stream instead after inserting the end of stream flush chunk.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-27 00:55:18 -08:00
Christopher Snowhill
7b702d23a6 Bug Fix: Do not perform cascading reset on DSPs
DSPs should not be performing a cascading reset when resetting just
their own buffers, for example, on init or shutdown of just that one
DSP filter.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-27 00:53:50 -08:00
Christopher Snowhill
4b13ca5be1 Bug Fix: Clear counter correctly on reset
This reset was missing.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-27 00:51:47 -08:00
Christopher Snowhill
814f65f830 FFMPEG: Optimize file reader access
Improve handling where FFmpeg may call the provided file reader with
AVSEEK_SIZE repeatedly, when file size is not likely to change between
repeated calls. This prevents repeated seek operations that would
otherwise be required to probe the file size each time.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-26 21:20:27 -08:00
Christopher Snowhill
d6cd240de6 Sentry: Replace most of old logging, add traces
Add event traces to playlist loading and metadata processing queues.
Unfortunately, most of the old non-error events should not be logged,
because Sentry gets terribly spammy with captureMessage events. They
should only be used for error events, or other uncommon events which
do not already throw exceptions or NSError objects.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-26 20:28:51 -08:00
Christopher Snowhill
440d2254be Sentry: Enable profiling for issue debugging
Enable processor usage profiling for the app to gather information for
potential bottleneck tracking.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-26 02:37:08 -08:00
Christopher Snowhill
6ddacf6e9d Bug Fix: Fix CI building again
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-26 01:23:38 -08:00
Christopher Snowhill
fd774d17a5 Feature: Replaced Crashlytics with Sentry
Crash logging is now handled by the Sentry service.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-26 01:11:55 -08:00
Christopher Snowhill
0c8f072deb Cleanup: Massive code cleanup and reorganization
Cleaned up project settings to current defaults, except for the macOS
deployment version, which is still 10.13. Cleaned up a lot of headers
and such to include with angle braces instead of double quotes. Enabled
build sandbox in a lot of places. Disabled subproject signing in several
places, for libraries and frameworks which will be stripped and signed
when they are copied into place in the final build.

Also, while trying to solve compilation issues, the visualization
controller was reverted to the Objective C implementation, which is
probably faster anyway. Stupid Swift/Objective-C language mixing issues.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-25 23:56:31 -08:00
Christopher Snowhill
4ed4ea906b Bug Fix: Attempt to make seeking more performant
Seeking should clear the buffers completely now, and will be nearly
instant, depending on how fast the input can decode.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-25 18:12:17 -08:00
Christopher Snowhill
53291b570d Bug Fix: Solve outstanding Equalizer bugs
This includes setting and unsetting the equalizer DSP chain objects on
track change and advancing on track playback end, and also bugs with
applying equalizer presets to the band configuration items when the
equalizer is disabled or when playback is stopped.

Fixes #420

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-23 21:16:57 -08:00
Christopher Snowhill
e76defbfd4 Bug Fix: Greatly improve seeking operations
Seeking now mutes properly, and will not leave the audio muted across
other operations. Audio output changes should also mute and destroy the
buffers of the input chain, so that the audio resets properly.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-23 19:58:56 -08:00
Christopher Snowhill
a7f1fd9d6c Bug Fix: Stage seeking operation on main thread
This should not interact with the Audio Player object on a background
thread, but instead the main thread queue.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-23 19:57:03 -08:00
Christopher Snowhill
9e24d60805 Bug Fix: Fix output volume from seeking
Fixes output volume setting on seek or audio output restart on format
change. Also safeguards these setters so they don't go off if the nodes
aren't actually allocated.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-23 18:58:44 -08:00
Christopher Snowhill
a744963548 Bug Fix: Playlist item out of range
This should never happen. But apparently it did.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-23 16:40:40 -08:00
Christopher Snowhill
ced4d73fd6 Crash Fix: Change background event to main thread
Two playback event items were set to queue a playback start to a
background thread, when playback should instead be queued on the main
thread. Fix this in a simple way.

This crash was easily reproducible by skipping through tracks rapidly.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-22 04:39:43 -08:00
Christopher Snowhill
03cce1b004 Bug Fix: Play Count data may be missing tags
Sometimes the play count data only includes the filenames, and thus will
fail a query for just the tags. Also, a file query may be stored without
the subsong fragment tag, which will also break the tags.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-20 01:25:44 -08:00
Christopher Snowhill
e97b96b3e9 Bug Fix: Clean up input node class definition
Fix some missing items, and add nullability declarations.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-19 15:02:44 -08:00
Christopher Snowhill
c85c149ceb Bug Fix: Crash fix sorting by several fields
Play Count sorting was entirely missing, and sample rate and bits per
sample sorting caused exceptions due to the capitalization of the fields
versus the column identifiers.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-19 14:54:51 -08:00
Christopher Snowhill
959fdf69a3 Bug Fix: Hopefully fix pasting a list of paths
This should hopefully fix pasting from a list of file URLs.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-19 14:53:47 -08:00
Christopher Snowhill
293a159116 TagLib: Fix framework Info.plist again
The TagLib framework build process leaves several
key fields empty. This breaks App Store submission.

Fix it again. Dang.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-17 19:19:00 -08:00
Christopher Snowhill
0fd4c327e4 VGMStream: Clean up FFmpeg code somewhat
Remove deprecated functions, make use of free functions that clear the
pointers before returning, etc.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-17 18:33:13 -08:00
Christopher Snowhill
b571c3f62a FFmpeg: Clean up code somewhat
Remove deprecated functions, make use of free functions that clear the
pointers before returning, etc.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-17 18:31:39 -08:00
Christopher Snowhill
80e909fbbe Bug Fix: Adding tracks to playlist while in search
When adding tracks to the playlist, clear the search filter first, so
the playlist doesn't become all jumbled, or so we don't overflow the
playlist indexes.

Also add some bug fixes for reversing the arranged to disarranged index
lists, so if an arranged index is past the end of the arranged list, as
is the case for appending, we shift the indexes forward past the end of
the diarranged object list.

Extra exception handling was added as well, so these things will only
cause a failure to add playlist items at worst, instead of crashing the
player entirely.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-17 16:06:08 -08:00
Christopher Snowhill
c74423c32d TagLib: Implement preliminary writer class
This is not currently being used anywhere, but may function eventually.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-17 04:25:15 -08:00
Christopher Snowhill
73161fdc12 TagLib: Update metadata readers
Update the readers to support the newly added tag fields, and also read
the supported format list from the library itself.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-17 04:23:29 -08:00
Christopher Snowhill
03bf4b36fe TagLib: Implement new field support
Implement the new fields into TagLib, pending contribution to upstream.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-17 04:21:32 -08:00
Christopher Snowhill
c4ed14aa53 Bug Fix: Hopefully fix flickering visualizations
Now buffer twice as much audio as would be requested for a single
visualization PCM/FFT chunk, which should hopefully prevent it from
flickering due to running out of audio because of too low latency.

Now it buffers up to two chunks at the current hard coded visualization
sample rate, which works out to about 186 milliseconds.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-16 14:19:43 -08:00
Christopher Snowhill
43b6a504b8 Minor Bug Fix: Handle Rubber Band buffer latency
We implement this function to return the current latency buffered,
regardless of how often this function may be called. In practice, it is
only called on track completion, to time the reporting of the next track
display. We also avoid using Rubber Band's latency function, as in most
cases, this function will be called from other threads, and also, it
currently only gets called after Rubber Band has been emptied out, so it
would otherwise calculate zero samples buffered. And thirdly, Rubber
Band's latency function doesn't account for the buffered samples already
removed from it and waiting to be fed out.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-16 14:06:15 -08:00
Christopher Snowhill
0262df7c53 Bug Fix: Rubber Band handles end of track gap
The code was polling the input chunk duration after emptying out the
chunk's samples, which resulted in an input duration account sitting at
exactly zero, so the end overrun flush would not be cut short properly,
resulting in gaps between tracks.

Correct the input sum to tabulate before emptying the input chunk, so
output remains properly gapless.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-16 14:03:27 -08:00
Christopher Snowhill
7bb6070350 Bug Fix: Set seek position when resuming paused
And a minor reoder of seek time reset code.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-15 21:46:57 -08:00
Christopher Snowhill
3f424cf5b0 Bug Fix: Rework playlist setup again
In case playlist setup is reset or not, move the reset above the menu
setup code, so the menu is set up correctly if a reset occurs.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-15 20:23:29 -08:00
Christopher Snowhill
df74a068dc Bug Fix: Change how bad playlist setup is handled
Reset to defaults if no columns are visible. Also log this situation in
Firebase events, in case it becomes relevant.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-15 20:09:59 -08:00
Christopher Snowhill
cd83dfa87c Audio: Unify playback setup of the converter
This code was being duplicated across three different playback functions
which basically did most of the same things.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-15 19:58:03 -08:00
Christopher Snowhill
40b2214c88 Bug Fix: Audio chain should do more error checking
Check all audio chain elements for allocation failures, and also dispose
of all of the previous handles in reverse order, including nulling the
final node handle so the output does not attempt to poll for audio while
the chain is being rebuilt.

Also set up output node to handle the new null finalNode state, and
return an empty chunk to the caller.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-15 19:56:05 -08:00
Christopher Snowhill
6fee16eb82 Bug Fix: Safeguard play count updates
Play Count cannot be updated for tracks which have been deleted before
the update was added to them. This was another cause of a rare crash.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-15 17:08:02 -08:00
Christopher Snowhill
b480453886 Bug Fix: Add safety checks to playlist columns
Playlist column setup needed a couple of safety checks to prevent
crashes from happening.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-15 14:09:27 -08:00
Christopher Snowhill
be0ccaffa2 Bug Fix: Do not process format change on stop
We should not be processing a potential playback restart when the chain
is being torn down for shutdown.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-15 01:33:06 -08:00
Christopher Snowhill
660d2b25be Bug Fix: Significantly rework Rubber Band DSP
This should be perfectly safe to use in all situations now. It may have
been unstable due to mishandling return values, or not supporting
requesting more sample data from the library without feeding in more
input first.

Also, still signaling the End of Stream flag on chunk reading should be
correct, as downstream processors only react to it when the buffer runs
empty.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-14 19:43:14 -08:00
Christopher Snowhill
0dee6e05ab Opus: Boost priority of libopusfile decoder
This should take priority over the Core Audio and FFmpeg decoders.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-14 19:07:31 -08:00
Christopher Snowhill
de6063780b TagLib: Re-enable some file types
These may be handled by the Core Audio input, which does not read tags
on its own.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-14 18:59:00 -08:00
Christopher Snowhill
82438fca04 Audio: Attempt to reduce glitching from seeking
Also applies to how output format changes are handled.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-14 18:50:50 -08:00
Christopher Snowhill
7088aae2e9 Bug Fix: Downmixer converter should update now
The Downmixer wasn't updating its output format correctly, so it was
prone to outputting the wrong format for a while, which could confuse
the output device and produce garbage output.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-14 18:46:47 -08:00
Christopher Snowhill
d2970e593d Crash Fix: Fix HRTF resampler delay misuse
The delay value should be scaled by the resampling ratio, similar to
how it already is when allocating the impulse buffer. This went
undetected, as it scribbled over other memory without causing immediate
crashes, but instead later heap corruption.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-14 18:45:18 -08:00
Christopher Snowhill
5424e18f27 Audio: Fix more hangs and resume playback on start
Check for paused processing state in various places, so that startup
playback works properly, and resume playback at seek offset works
properly and doesn't hang the player.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-13 22:25:36 -08:00
Christopher Snowhill
94fcb68563 Rubber Band DSP: Fix error checking for output
The samples available function returns a signed integer, so it can
apparently return negative on error, and the DSP was incorrectly casting
this to an unsigned type, and thus attempting to buffer an inordinate
number of samples and crashing.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-13 20:58:07 -08:00
Christopher Snowhill
2ba8ec04a2 Visualization: Optimize Swift code handling arrays
This looks a lot better than some ruddy for-loops.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-13 19:58:20 -08:00
Christopher Snowhill
b0e6ec98a9 Audio: Improve buffer signaling
This should stop the deadlocks which were occurring.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-13 19:55:39 -08:00
Christopher Snowhill
4cd5cb8fa7 Downmix: Move downmix to DSP chain and fix a bug
The downmix filter also had a bug related to the channel configuration
used by the HRTF filter.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-13 14:56:18 -08:00
Christopher Snowhill
d3778f92fc Audio: Make chunk merging abortable
The merge function should be able to tell when the caller has no audio
left to process, such as on end of stream.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-13 13:51:46 -08:00
Christopher Snowhill
f3132e0061 Equalizer: Fix to function properly
This was completely broken, oops.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-13 13:39:16 -08:00
Christopher Snowhill
8647f76a46 Audio: Increase buffering before FreeSurround
FreeSurround needs more buffering from its input, so increase buffering
of previous node to 100ms.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-13 06:35:26 -08:00
Christopher Snowhill
c3af7c3bdc Audio: General fixes and improvements
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-13 06:34:21 -08:00
Christopher Snowhill
c48a52cda3 Bug Fixes: Fix monotonically increasing timestamps
Fixes timestamps in several cases where they were being processed
incorrectly, which was causing some chunked audio files to mis-report
timestamps into the past or the future, which caused the seekbar to jump
around in an unpredictable way.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-13 03:26:59 -08:00
Christopher Snowhill
96a79d3ff2 FFmpeg Input: Fix stream timestamps
Stream timestamps were correctly being converted from the monotonically
increasing frame count, but the AudioChunk parameter was being set from
the frame count rather than the converted seconds count.

Fixes #418

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-13 03:25:43 -08:00
Christopher Snowhill
061eefee29 Audio Node: Revert timedWait usage
Timed wait for 500us is kind of stupid and makes the threads wake up way
too much, and use way more CPU time. Reduce this, as the semaphores are
signaled appropriately, and the waiter should not wake up constantly.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-13 02:23:44 -08:00
Christopher Snowhill
fb5c23461d Visualization: Clean up Swift code a bit
Some of this is handled in simpler ways now.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-13 02:09:08 -08:00
Christopher Snowhill
9dcc434992 Visualization: Reworked buffering system
Visualization now buffers in the audio output pipeline, and uses a
container system to delay multiple buffer chains from emitting
visualization data over top of each other. This should stabilize
display output significantly, while introducing minimal lag before
DSP configuration changes take effect.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-13 01:12:53 -08:00
Christopher Snowhill
08539a81e4 Visualization: Do not increment latency on write
The latency should not be incremented when writing sample data to the
buffer, but rather be posted by the output.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-13 01:06:07 -08:00
Christopher Snowhill
b53ebc08fe Audio: General cleanup and empty chunk checking
Upstream functions which return empty chunks on error do not return nil,
so the caller should check for an empty duration instead.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-13 01:05:15 -08:00
Christopher Snowhill
3eec6d7700 Audio Chunk: Add interface to copy chunk
This is needed if audio is to be removed from the chunk without altering
the original chunk.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-13 01:01:52 -08:00
Christopher Snowhill
139ff3a2b8 HRTF DSP: Add gain correction to impulse resampler
Impulses should be gain scaled roughly based on the sample ratio
relative to the original impulses. Lower target sample rate means less
impulses means gain goes up, higher target sample rate means more
impulses so gain goes down. Somewhat simple, seems to work.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-12 21:08:25 -08:00
Christopher Snowhill
de29e33714 Audio: No longer force output sample rate
We were forcing a resampling ratio to match the HRTF filter supplied
with the app, now we resample the HRTF to match the input audio, which
will be resampled to match the output device settings.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-12 20:59:59 -08:00
Christopher Snowhill
be177617d3 HRTF DSP: Support resampling impulses
This prepares the filter to be the same as the rest of the filters, in
that they support flexible sample rates to match the output device.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-12 20:59:55 -08:00
Christopher Snowhill
afd2ca2e2a Rubber Band DSP: Make it possible to disable it
And disable it by default in new installations, otherwise leave the
setting alone. The disablement setting is shared with the engine
setting, so the default should not really change anything, except for
new installs.

Also, the time/pitch shifting dialog disables itself and displays an
obvious notice button, which opens the Rubber Band settings.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-12 20:11:05 -08:00
Christopher Snowhill
5e4dd125dd Crashlytics: Add consent preferences defaults
These should have been defined already, but now they're the safe
defaults that should spring the dialog on startup, and doesn't grant
consent by default.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-12 20:04:45 -08:00
Christopher Snowhill
7101527585 Cleanup: Whitespace removal
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-12 19:01:10 -08:00
Christopher Snowhill
9589a08724 Audio Output: Set higher priority on output thread
It's more like the output monitor thread, since it only monitors output,
rather than actually handing the output callbacks.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-12 19:00:52 -08:00
Christopher Snowhill
eaaabafdd2 Audio Processing: Unify sample block merging code
Sample block merging code should not be duplicated across the DSPs that
require it, but instead should be a common function. Also added some
optimizations to the Float32 converter function, to bypass conversion if
the audio format needs no conversion.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-12 18:54:38 -08:00
Christopher Snowhill
6f6c08dc5b Rubber Band DSP: Process larger blocks at a time
Attempt to completely fill the input buffer of the Rubber Band library
between each call to the process function, instead of processing in
as small an increment as the source node provides. May reduce processing
power required.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-12 14:56:32 -08:00
Christopher Snowhill
ee7aae922d Audio: Add full timestamp accounting to playback
Audio Chunks now have full timestamp accounting, including DSP playback
speed ratio for the one DSP that can change play ratio, Rubber Band.
Inputs which support looping and actually reporting the absolute play
position now do so.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-12 14:08:17 -08:00
Christopher Snowhill
b858a48032 Bug Fix: Fix resume playback on startup
In case multiple playlist entries are left marked as "current" in the
playlist database, resume playback on the first one, and unmark all the
rest of them.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-11 23:04:35 -08:00
Christopher Snowhill
92dbe351af DSP: Add format change checking to FreeSurround
FreeSurround, like the Equalizer, which attempt to coalesce Audio Chunks
into larger blocks of 4096 samples, must check if the audio format has
changed between blocks, and stop stacking chunks together when a new
format is detected. They will continue processing with less sample data
than expected, as necessary.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-11 23:04:31 -08:00
Christopher Snowhill
ce2bedf478 DSP: Move Equalizer processor to DSP node chain
The last of the built-in processors is now in the threaded processing
chain, and all DSPs are marked high priority and with short buffers.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-11 23:04:26 -08:00
Christopher Snowhill
8769869aee DSP: Move HRTF filter to DSP class chain
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-11 23:04:22 -08:00
Christopher Snowhill
d8802bc0da DSP: Move FreeSurround to DSP chain
This will no longer be in the output implementation.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-11 23:04:18 -08:00
Christopher Snowhill
c19de448dc DSP: Move Rubber Band to its own DSP group
This is a project file structure change only, no code changes.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-11 23:04:13 -08:00
Christopher Snowhill
d6b7ed467e Output: Remove pointless scale value
This shouldn't have been applied, the problem with Rubber Band was the
flushing mechanism.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-11 18:12:06 -08:00
Christopher Snowhill
8f1ef5eb6b Audio: Adjust node buffering behavior a bit
Change one remaining semaphore wait to 500us, and change the buffering
so that it can always overflow the requested duration by one chunk, so
that at least one chunk will always fit in the buffer. This also allows
the DSP nodes to flush at the end of the stream without losing their
output.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-11 18:11:26 -08:00
Christopher Snowhill
108186450f Rubber Band: Handle end of stream flushing better
The end of stream flushing should only request remaining samples once,
as should the rest of the process. The problem with the Rubber Band code
in this case is that it will wrap the remaining samples pointer after it
has been flushed, and emit a really huge number.

Also, add code to try to equalize the samples output with the samples
input, relative to the tempo stretching, as Rubber Band seems to flush
entirely too much data at end of stream, which can create noticeable
gaps in the output. This solves that as well.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-11 18:09:04 -08:00
Christopher Snowhill
08b991a7c3 Rubberband DSP: Guard non-restart config function
This should be guarded, so that no other thread tries to free the DSP
while it is potentially writing to the Rubber Band instance.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-11 17:50:07 -08:00
Christopher Snowhill
ec7d936289 DSP: Stylistic change
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-11 17:45:05 -08:00
Christopher Snowhill
7e5c9c8f7c DSP: Whitespace changes
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-11 17:44:07 -08:00
Christopher Snowhill
8a468c08ce Cleanup: Remove stale comment from source code
This comment was copied by accident when duplicating the original
Converter Node class for the new DSP base.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-11 15:10:30 -08:00
Christopher Snowhill
9ef21d8185 DSP: Add thread priority control
DSP threads, such as the Rubber Band processing, and planned moves of
other processing to buffer threads, such as the Equalizer, FreeSurround,
HRTF, and Downmixing for output, because they all have small output
buffers. Since these buffers drain and fill fast, they should be
processed at a high priority. Hopefully, App Store doesn't complain
about the use of these APIs.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-11 15:06:59 -08:00
Christopher Snowhill
2b3d622685 Preferences: Fix merge error
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-11 13:57:45 -08:00
56cf509670 Changed term in Spanish translation
Yes, it's mostly used untranslated now...
2025-02-11 13:48:10 -08:00
Christopher Snowhill
1efdea953c Rubber Band: Fix preferences disabling items
The items not applicable to Finer / R3 engine were not being disabled
properly. Change the dialog to use a transformer to disable them on the
preferences value instead of coding it, since the code didn't seem to
work.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-11 01:28:59 -08:00
Christopher Snowhill
9df263e87b Rubber Band: Move default preferences
Move them to the main app instead of an external module.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-11 01:28:54 -08:00
Christopher Snowhill
4fefdc7ea3 Rubber Band: Move everything to a DSP class
This class can more flexibly process and emit varying chunk sizes than
the previous code could, solving the problem of wide tempo changes.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-11 01:23:06 -08:00
24f9ca9214 Spanish translation for Rubber Band settings, and
for rubber band icon credits.
2025-02-11 00:55:48 -08:00
Christopher Snowhill
76d42f2c08 Fix up team identifiers again
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-10 14:48:17 -08:00
Christopher Snowhill
0498eb5f81 Rubber Band: Implement configuration dialog
Now there's a configuration dialog for tweaking the settings
in semi-real time. Everything that can be changed without
restarting is changed without restarting, otherwise the audio
pipeline is reset, which happens quickly enough anyway.

Awaiting translation to Spanish, other languages have been
removed pending their maintainers fixing most of their
problems, which includes me being lazy and AI translating
bits so I could rush updates.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-10 14:42:41 -08:00
Christopher Snowhill
37f1be354f Core Audio: Fix API header
Fix a function declaration that was missing its
parameter variable in the header.
2025-02-10 14:40:20 -08:00
Christopher Snowhill
47b4d19f8f Revert "Visualization: Tweak systems a bit"
This reverts commit 88f370ed91.
2025-02-10 14:40:08 -08:00
Christopher Snowhill
ce91cc5d6c Updated VGMStream to r1980-95-g551c0787
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-10 14:38:48 -08:00
Christopher Snowhill
b1deca3fcf Remove unmaintained translations
The Polish, Russian, and Turkish translations have
no active maintainers, so I was stupidly relying on
AI translation to fill in the newer things. These
translations will sit mostly idle until I get active
maintainers for them.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-10 14:38:19 -08:00
Christopher Snowhill
e9883f38af Updated Sparkle to version 2.6.4
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-06 20:58:52 -08:00
Christopher Snowhill
88f370ed91 Visualization: Tweak systems a bit
This should improve performance slightly. It's
still recommended to switch off SceneKit to
save CPU usage, or switch of vis entirely.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-01 15:08:25 -08:00
Christopher Snowhill
d8c9236b2f Updated credits file and Patron list
Been a while since I did this. Oops.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-02-01 14:50:01 -08:00
Christopher Snowhill
8786f2b21e TagLib: Fix up project file
Gah, how in heck did Xcode end up inserting an absolute
path? Fix that, and some other leftovers.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-01-30 02:18:16 -08:00
Christopher Snowhill
f3d3a5ca4e TagLib: Fix framework Info.plist
The TagLib framework build process leaves several
key fields empty. This breaks App Store submission.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-01-30 01:45:24 -08:00
Christopher Snowhill
034b0d20b8 SID: Add exception handling
Exception handling was quite missing from this code
as well. Let's fix that too.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-01-30 01:16:58 -08:00
Christopher Snowhill
8eddb7bf40 OpenMPT: Update exception handling
Make exception handling more robust and thorough. Never
know what may happen, make sure to handle most cases.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-01-30 01:02:32 -08:00
Christopher Snowhill
fd3e9e2b24 MIDI: Add exception handling
Both the midi_processing and the various players may
throw exceptions, so we should check for these too.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-01-30 00:52:26 -08:00
Christopher Snowhill
8f91a49bca Highly Complete: Add exception handling for NCSF
SSEQPlayer throws exceptions, there should be exception
handling to catch them and fail gracefully.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-01-30 00:38:21 -08:00
Christopher Snowhill
e9a17a8ba5 TagLib: Add exception handling
The TagLib C++ code was missing generic try/catch handling
which could result in any generic errors throwing straight
to a full crash. Add exception handling and logging, which
will fix a logged crash regardless of whether the tags are
read correctly or not by the newer TagLib version.

Fixes #415

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-01-29 23:57:12 -08:00
Christopher Snowhill
61165a022a TagLib: Replace bundled copy with upstream 2.0.2
Include a Framework build, unmodified, RelWithDbgInfo.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-01-29 23:41:00 -08:00
Christopher Snowhill
6bbec09b8d Updated VGMStream to r1980-54-g35c8283f
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-01-26 01:16:52 -08:00
Christopher Snowhill
0a298e1d71 Playlist View: Save column settings differently
You will need to reset your settings after this, but then it should
stay put for the indefinite future.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-01-26 00:52:58 -08:00
ad4672b150 Update bug_report.md 2025-01-20 14:20:10 -08:00
Christopher Snowhill
d2bb1458cc Visualization: Fix race condition on first launch crashing
Due to a race condition with the visualization control racing with
the Crashlytics consent dialog, it was possible that the repaint
function would be called before the control was fully initialized,
which would cause the visualization drawing code to crash due to
division by zero error.

The fix is two-fold: First guards were added to the borrowed
code so that the draw functions won't run if they would later
divide by zero on an uninitialized width property. Secondly, the
top level visualization windows added a startup variable guard
so their drawing code will return immediately if setup has not
completed yet.

Note that this bug was only just noticed in a recent App Store
submission, but was unrelated to the recent commits to the code
base, and could have triggered much earlier in the development
cycle. Strangely, it did not.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-01-15 17:47:49 -08:00
Christopher Snowhill
f33e0138e4 Playlist: Added play count column
It should also be possible to sort by the column, ascending or
descending. This also necessitated adding playlist row refreshing
for play count updates.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-01-14 23:37:17 -08:00
Christopher Snowhill
8102731228 Highly Complete: Fix crash on paths containing URL escapes
In the event of local paths containing not just UTF-8 characters,
but also un-decoded URL percent sequences, which will end up double
encoded in the player, code which reverses percent encoding should
later re-apply it.

Apparently, this whole time, since the last code overhaul, the
URL encoding was being stripped, then the file opener was converting
these paths back into URLs without re-encoding, which didn't break
until someone played an album in a folder containing a partially
decoded UTF-8 sequence. Thanks, Zophar's Domain, and whoever ripped
the Golden Sun GSF set for finding this bug!

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-01-14 21:22:26 -08:00
Christopher Snowhill
9ed4e4e8d9 Updated libOpenMPT to version 0.7.13
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-01-10 14:39:38 -08:00
Christopher Snowhill
4b19c15354 Dependencies: Updated mpg123 to 1.32.10
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-01-10 14:39:29 -08:00
Christopher Snowhill
ae71a6a2bc Audio/HRTF: Make head tracking optional, add reset button
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-01-03 15:23:02 -08:00
Christopher Snowhill
160c7e43b7 Updated VGMStream to r1980-0-ged9a7202
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-01-03 02:30:56 -08:00
Christopher Snowhill
cc79342c5b Audio/extrapolator: Fix short prime length
When the input buffer has less samples in it than the LPC order,
it would crash reaching past the ends of the buffer. Now, it will
pad past the correct end of the audio with silence, while still
extrapolating a prime input minimum of the LPC order. Should fix
the last of the outstanding crashes.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-01-03 02:20:56 -08:00
Christopher Snowhill
91b31255e6 Notification: Fix album date assignment
The NSCalendar assignment should have a placeholder month and day
of January 1st, instead of the invalid month/day of 0/0. Also,
even if this somehow fails, don't attempt to assign it if it
returns nil.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-01-03 02:20:49 -08:00
Christopher Snowhill
925a3502fa FileTree: Fix handling of metadata with multiple values
Goody, my metadata formatting comes back to bite me.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-01-03 02:20:42 -08:00
Christopher Snowhill
0b39b57f61 Chore: Update copyright dates somewhat
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2025-01-01 01:30:48 -08:00
Christopher Snowhill
408b8047bf FFmpeg: Move some error buffers around
Since in one case, it probably wasn't combining them into one stack
allocation, it probably blew up the stack allocation quite a bit.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2024-12-09 17:58:58 -08:00
Christopher Snowhill
e5eeb987fa Implemented real pitch and time shifting using Rubber Band
I will implement the more complex setup of providing options for
most of the configuration that Rubber Band provides, at a later
date, when I feel like creating a complex configuration dialog
for it, and asking for help translating every option and setting.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2024-12-09 00:44:43 -08:00
Christopher Snowhill
fb0aae1dea Updated VGMStream to r1951-102-gf1483e22
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2024-12-04 23:09:43 -08:00
Christopher Snowhill
75a4f68feb Updated libOpenMPT to version 0.7.12
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2024-12-04 23:04:11 -08:00
Christopher Snowhill
18fe6c0563 Minor corrections to Polish translation
mpan told me these were broken, oops. One of them
may as well not be translated until it gets a proper
translation.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2024-11-24 21:29:43 -08:00
Christopher Snowhill
ab798fd86a Add support for custom Dock icons while running
The emoji labeled buttons will convert and save their respective
state icon to the settings folder, and refresh the current icon
as necessary.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2024-11-24 21:28:34 -08:00
Christopher Snowhill
78fa70a770 Updated VGMStream to r1951-100-g73ef7c6c
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2024-11-24 15:08:17 -08:00
Christopher Snowhill
95c9f91120 Fix FreeSurround being broken by the speed control
It should be deriving its channel count from the file format,
since it's applied before any other filters.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2024-11-24 15:07:04 -08:00
Christopher Snowhill
58cbda594a Updated libOpenMPT to version 0.7.11
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2024-10-30 23:57:33 -07:00
Christopher Snowhill
6b8f5df721 Updated VGMStream to r1951-88-g02d3c3f8
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2024-10-30 23:56:21 -07:00
Christopher Snowhill
2ac35a0b87 Reorder project file entries with a sort 2024-09-20 22:24:37 -07:00
Christopher Snowhill
bd457f91e9 Remove unused variable 2024-09-20 22:24:14 -07:00
Christopher Snowhill
27c5e50633 Speed Control: Implement simple speed control
Implements a simple speed control using a resampler
designed for real time changes. A rubberband speed
control will be implemented at a later date.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2024-09-20 22:23:59 -07:00
Christopher Snowhill
42a7232fda Visualization: Make latency animation smoother
Compensate for latency by incrementing an offset
according to animation frame rate.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2024-09-20 22:19:36 -07:00
Christopher Snowhill
26bdd7fcc0 Update Github workflow
Update checkout and upload artifacts to latest versions
2024-09-17 02:41:49 -07:00
Christopher Snowhill
6c339bb9f9 VGMStream: Fix fade time configuration
Fade time was reading from some completely wrong
configuration field name, so fades weren't working.
2024-09-17 02:24:13 -07:00
Christopher Snowhill
9fa9274ce5 VGMStream: Change render API, allocate off heap
Change to the future render api, hopefully float support
will be available eventually. Also change to allocate the
sample buffers from the heap instead of the stack.
2024-09-17 02:23:36 -07:00
Christopher Snowhill
5c8441eb22 Updated VGMStream to r1951-50-g1d836a36 2024-09-17 02:19:58 -07:00
Christopher Snowhill
8498bba881 Updated VGMStream to r1951-0-g4b2dc01c
Updated to add one new extension, too.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2024-08-23 23:06:59 -07:00
Christopher Snowhill
ee98898dbc Downgrade SwiftPM pins format version number
Apparently Github's old ass version of Xcode can't handle a 3 instead
of a 2 here. Whatever.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2024-08-18 04:11:54 -07:00
Christopher Snowhill
50b3649ce6 Downgrade Firebase to version 10.x
Version 11.0 requires macOS 10.15, and I haven't raised the minimum
deployment version that far yet.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2024-08-17 15:35:24 -07:00
Christopher Snowhill
952ca1b42d Update Firebase SDK to version 11.0.0 minimum
This should fix the Framework packaging issues App Store noticed.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2024-08-17 15:30:08 -07:00
Christopher Snowhill
62ab217c28 Update Copyright year to 2024 manually
Including extending existing starting-2023 dates to ranges.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2024-08-17 05:06:58 -07:00
Christopher Snowhill
e03d857f86 Updated libOpenMPT to version 0.7.9
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2024-08-17 03:33:43 -07:00
Christopher Snowhill
681cd18145 Update PluginController.mm
Add missing definitions to the Info.plist template generator.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2024-08-17 03:22:34 -07:00
Christopher Snowhill
ab00010778 Update Info.plist.template
Clean up duplicate definitions.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2024-08-17 03:21:48 -07:00
Christopher Snowhill
626a9e2205 Updated VGMStream to r1917-201-g7ab622d3
Also updated filename extensions,  and the interface plugins.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2024-08-17 03:20:11 -07:00
Christopher Snowhill
7b2fcf7c94 Updated Sparkle to version 2.6.0 2024-08-07 23:16:47 -07:00
Christopher Snowhill
b2db8d3bac Update FFmpeg to version 7.0 and rebuilt soxr
Rebuilt libsoxr, which now removes the dependency on libavutil.
2024-08-07 23:15:51 -07:00
Christopher Snowhill
0f5f11a5a4
Fix crash on unaligned volume scale
Volume scaling would potentially crash when handling
unaligned blocks of samples, and also handled them
completely wrong. It should be counting up single
samples until the buffer is aligned to a multiple of 16
bytes, and it should not exceed the intended count.

BUG: It was not only counting the unaligned samples
backwards, it was ignoring the real sample count.

Fixes #380

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-10-11 20:22:17 -07:00
Christopher Snowhill
587b7900b5
Hopefully fix memory usage during playback
Shuffle around @autoreleasepool blocks, and also add one
to the audio processing code in the playback callback, so
audio memory is released during playback instead of
accumulating.

Fixes #379

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-10-11 20:22:14 -07:00
Christopher Snowhill
f52de150a7
Processing: Fix missing converter setup function
Oops, I was missing a function necessary for output format changes.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-10-04 16:07:20 -07:00
Christopher Snowhill
dc1f78fe4f
CI: Hopefully fix this time
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-10-04 02:10:53 -07:00
Christopher Snowhill
7634283717
Add a cache shutdown guard
This appears to maybe be necessary as the prior join call doesn't seem to
be doing what it should.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-10-03 22:59:23 -07:00
Christopher Snowhill
ba8a10f494
Update libFLAC to version 1.4.3
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-10-03 22:58:49 -07:00
Christopher Snowhill
7ee6359056
CI: Switch to Xcode 15
Apparently, the build images are broken or something.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-10-03 22:01:30 -07:00
Christopher Snowhill
17c598878c
CI: Switch to latest macOS and latest stable Xcode
Switch the CI image to the latest stable OS version, and latest stable
Xcode version, selected by another action.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-10-03 21:29:41 -07:00
Christopher Snowhill
c4df174ee6
Remove redundant track end checker
This is checked inside the audio thread, it isn't needed in the watcher
thread. Remove the second check.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-10-03 21:28:44 -07:00
Christopher Snowhill
38dfd2f4a2
Added Usage Description translations
Spanish translation provided by Kevin López Brante.
Polish, Russian, and Turkish translations of specifically these messages
provided by GPT-4, to be replaced by human translations if our
translators offer them, but it's been a while since I've gotten an update
out and I didn't really want to wait so long.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-10-03 20:35:35 -07:00
Christopher Snowhill
a37026dec2
Reduce audio buffering slightly again
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-10-03 19:46:11 -07:00
Christopher Snowhill
8e90da6292 Visualization: Improve latency and buffering appearance
Adjust the buffering so if latency is too low, we fill the rest of
the output with silence instead of peeking at the oldest part
of the buffer. Also increase latency by half a buffer size so
that the requested sample is in the center of the buffer, which
improves the 4096 sample situation with the current low
latency output.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-10-03 19:34:42 -07:00
Christopher Snowhill
416e77d220 Visualization: Reset buffer on playback stop
Reset the visualization system when stopping playback.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-10-03 19:32:40 -07:00
Christopher Snowhill
0d682fef37 Initial implementation of positional audio for macOS Sonoma
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-10-03 04:59:54 -07:00
Christopher Snowhill
fbde40212c Replace hard coded Pi constant with M_PI
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-10-03 04:55:44 -07:00
Christopher Snowhill
a1bd2e0d44 Significantly improve memory usage of loading tags
This especially helps with bad or brutal files.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-10-03 04:55:09 -07:00
Christopher Snowhill
34edc003db Fix a typo
Notifcation -> Notification

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-10-03 04:53:26 -07:00
Christopher Snowhill
d221300fb6 Improve audio buffering situation
Buffer up to 20 seconds per stage, and buffer only up
to 2 seconds before starting the next stage.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-10-03 04:48:57 -07:00
Christopher Snowhill
791c9f9b17
Stop visualizer feed for stopped playback
A stopped instance of OutputCoreAudio should not continue to feed the
visualization system with stale audio, potentially while another instance
is already starting up and feeding its own audio output.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-10-02 19:15:58 -07:00
Christopher Snowhill
73738aa185 Fix a missing do on a do-while block
This should be looping on the condition, not sure
how the compiler missed this one.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-10-02 19:14:36 -07:00
Christopher Snowhill
e9e4fd3aa4
Hopefully fix format change on end of track
This should keep the audio pipeline flowing either way.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-10-02 19:13:27 -07:00
Christopher Snowhill
4d0123e13d
Simplify HRTF filter, change option to reflect it
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-10-02 10:58:13 -07:00
Christopher Snowhill
2eabd72491
Attempt to stabilize visualization flutter
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-10-02 10:58:09 -07:00
Christopher Snowhill
f4f46942ec
Fix converter after output switchover
This was missing, too.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-10-02 10:58:04 -07:00
Christopher Snowhill
adc4128c28
Fix further bugs with output switchover
Change some things I missed.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-10-02 10:58:00 -07:00
Christopher Snowhill
d364a7ef10
Update BASS and friends for Xcode 15
This is needed for the Intel build target to work.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-10-02 10:57:55 -07:00
Christopher Snowhill
661f047e1b
Add missing Info.plist strings
These strings were added to the InfoPlist translations in a
previous commit, but likely need to be added to the
Info.plist file directly as well.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-10-02 10:57:50 -07:00
Christopher Snowhill
7ac32284ff
Revert to previous low latency output system
This reverts usage of the AVFoundation output to use
the previous lower latency CoreAudio output, and
paves the way for a change I am cooking up soon.

Fixes several issues with playback and seeking latency.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-10-02 10:57:47 -07:00
Christopher Snowhill
ca1f9381b5
Hopefully fix crashes from rapidly skipping files
Do this by serializing the background thread actions against
the AudioPlayer object, so we don't start playback multiple
times at once.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-10-02 10:57:42 -07:00
Christopher Snowhill
cda6ce608c
Privacy - Added Spanish translation
Translation provided by team member, Kevin López Brante.
2023-09-03 16:52:40 -07:00
Christopher Snowhill
d3a31da1a6
Privacy: Added purpose strings
The other translations currently have placeholders, awaiting translation.
I may end up using machine translation from Deepl just for Russian,
Polish, and Turkish, until I get human translations from a contributor.
Spanish will be provided by our team later tonight.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-09-03 16:38:53 -07:00
Christopher Snowhill
8d2425b06a
Playback: Start playback and seek in the background
Perform playback start and seeking operations in the background, instead
of on the main thread, which should help prevent them from stalling the
user interface.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-09-02 22:28:15 -07:00
Christopher Snowhill
d069aa0361
Updated Translations
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-09-02 21:40:53 -07:00
Christopher Snowhill
f345f1ddf0
Touched by Xcode
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-09-02 21:40:36 -07:00
Christopher Snowhill
08c904c201
Updated VGMStream to r1866-46-g883d796d
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-09-02 21:18:22 -07:00
Christopher Snowhill
c802dbfa45
MainMenu: Touched by Xcode
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-09-02 21:17:59 -07:00
Christopher Snowhill
b64e00449e
AppleScript: Implemented Composer field
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-09-02 21:17:31 -07:00
Christopher Snowhill
4c9895d208
Info Inspector: Implemented Composer field
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-09-02 21:00:25 -07:00
Christopher Snowhill
750fae6712
Playlist: Implemented Composer column
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-09-02 21:00:23 -07:00
Christopher Snowhill
4ac4881e4c
Tags: Expose Composer tag through interfaces
Implement the composer field interface to playlist item tag
reading and writing.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-09-02 21:00:20 -07:00
Christopher Snowhill
4bb59c1c3e
TagLib: Remove stray whitespace
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-09-02 21:00:18 -07:00
Christopher Snowhill
248718c967
TagLib: Implement Composer tag support
Most of the reading was already there, it just didn't expose
it to the player.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-09-02 21:00:15 -07:00
Christopher Snowhill
b9121be6e8
ASF/WMA: Implement Album Artist tags (oops)
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-09-02 21:00:13 -07:00
Christopher Snowhill
7ea87f0502 Add a bitrate column to the playlist
Adds a bitrate column to the playlist view, which is fully sortable.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-08-20 23:47:37 -07:00
Christopher Snowhill
16c1cf4b65 Fix rating sorting
This apparently fixes sorting by rating.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-08-20 23:46:00 -07:00
Christopher Snowhill
f62a7c5fed Add missing column header translations
These column header names were missing from the strings
tables. Add them based on existing translations of the same
field names by our translators.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-08-20 23:36:42 -07:00
Christopher Snowhill
59123f98d5 Touched by Xcode
Miscellaneous XIB changes automatically applied by Xcode
when editing.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-08-20 23:35:09 -07:00
Christopher Snowhill
158fbed081 Fix sorting for missing columns
This should fix the sorting for rating, sample rate and bits per sample

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-08-20 23:16:47 -07:00
Christopher Snowhill
0b75967d64
Attempt to make project build with Xcode 15
This is supposed to fix running on macOS 10.13, but it doesn't.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-08-20 23:15:38 -07:00
Christopher Snowhill
545a1e9632
Reduce inter-thread buffering a bit
This isn't needed so much now that the output buffers more.

Should reduce the problems of #370

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-07-29 01:31:26 -07:00
Christopher Snowhill
0c934886f4
Don't pause streams, stop instead
Do not bother to pause streamed files, instead stop playback when pause
is requested.

Fixes #372

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-07-29 00:56:16 -07:00
Christopher Snowhill
20531b2d04
Fix Repeat menu being disabled, change default
Default repeat mode should now be Repeat All, and the menu items should
now all function, fixed by removing a bunch of pointless attributes from
each affected menu item.

Fixes #371

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-07-29 00:55:14 -07:00
Christopher Snowhill
d606790ae3
Touched by Xcode
Resource scripts touched by Xcode again

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-07-29 00:49:01 -07:00
Christopher Snowhill
056bb2bd26
Stash a Core Audio output, kind of glitchy
This output may prove to have lower latency, but the results are too
glitchy to really be usable. Not even visualization latency is handled
correctly.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-07-29 00:23:58 -07:00
Christopher Snowhill
38ad84e86c
MIDI: Properly bundle the BASS Musepack plugin
This is imported by the player, though it doesn't fail when it's
missing, so that would explain why I didn't spot this error sooner.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-07-21 02:41:01 -07:00
Christopher Snowhill
c2fa2dcaf6
MIDI: Stop linking BASS plugins directly
They'll be imported by the plugin on startup anyway, and they don't
really have any exported functions we can use.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-07-21 02:40:09 -07:00
Christopher Snowhill
5239c1a5c8
Disable dead code stripping
No idea why this was enabled, no idea if I should disable it.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-07-21 02:38:53 -07:00
Christopher Snowhill
a81f3f05d9
APL+CUE: Revert pointless change
This change served no purpose, other than to confuse me. I must need
some sleep already.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-07-21 00:24:59 -07:00
Christopher Snowhill
cc0f55e3c4
APL: Stop on the correct sample
This should stop before the specified end point, not one sample later.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-07-21 00:19:52 -07:00
Christopher Snowhill
6cda7696f3
APL: Fix position handling for DSD
Correctly scale the AudioChunk frame counts. This more and more makes me
think I should be scaling this in the AudioChunk code instead, but then
code may not know about the special case of every 8 frames only being
one byte per channel.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-07-21 00:18:20 -07:00
Christopher Snowhill
7f0fe33f9d
APL: Round bits per sample to an even byte
Round this variable up.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-07-21 00:14:31 -07:00
Christopher Snowhill
503ca803e9 CUE: Fix playback position tracking for DSD
DSD wasn't tracking the correct sample count, because DSD
Audio Chunks store the byte count, rather than the bit count.
This may be changed in the future, so I'll have to remember.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-07-21 00:12:43 -07:00
Christopher Snowhill
bb21da6ed1 CUE: Stop on the correct sample
Stop before the end, rather than on it.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-07-21 00:11:12 -07:00
Christopher Snowhill
2ac9741ef2 CUE: Round up bits per sample
Round bits per sample to an even byte.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-07-21 00:10:13 -07:00
Christopher Snowhill
c9a5bf26f9
Add status messages to updater script
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-07-21 00:06:40 -07:00
Christopher Snowhill
28b36dda66
Add an option to control halving DSD volume level
And default it to disabled. As was pointed out to me by a user, DSD is
apparently mastered to a level of -6 dB, so double its level on output
by default.

Also reorder all preferences dialog controls so they are instantiated in
display order, which should help screen readers, maybe.

Fixes #368

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-07-15 16:46:30 -07:00
Christopher Snowhill
3d00e8dd2d
Work around rounding error with resampler flush
Resampler flush may indefinitely produce 1 sample if there is a rounding
error with the buffering calculations. Work around this.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-07-14 05:16:12 -07:00
Christopher Snowhill
a6ecd7eed9
Fix clipped sample rate changing between files
When the clipped sample rate changes, the resampler needs to be
restarted. This was previously failing because the target sample rate
wasn't changing.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-07-14 05:15:14 -07:00
Christopher Snowhill
c9ed3c4817
Fix lossless capability reporting for partial read
When reading partial chunks, and when returning partial data, it is
essential to maintain this lossless chunk status across either whole or
partial chunk reads. Otherwise, the converter chain sees the lossless
flag constantly changing on lossless files, such as PCM or DSD, and
causes the DSD decimator and/or resampler to be torn down and reset
repeatedly, causing glitches in the audio.

The glitch was not, in fact, with the decimator itself, and was
occurring to a degree without it, as it would be restarting the
resampler repeatedly as well.

Fixes #367

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-07-14 04:14:35 -07:00
Christopher Snowhill
24a3209682
Correct the decimator sample latency
The latency is half of the FIFO, or half the filter size, and each byte
is 8 samples, so return the value accordingly.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-07-14 04:14:30 -07:00
Christopher Snowhill
555b9f248d
Add an explanatory comment that got lost
This comment was in the original sample decimator code, I neglected to
include it in my port over to Cog. Doesn't really serve any functional
change, though. It would have clarified that I needed to reduce the gain
level much sooner, though.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-07-14 04:14:26 -07:00
Christopher Snowhill
b2657700eb Updated libOpenMPT to version 0.7.2
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-06-30 23:28:42 -07:00
Christopher Snowhill
69251314c4 Updated VGMStream to r1843-92-g740a4048
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-06-30 23:17:55 -07:00
Christopher Snowhill
d103d78fc3 Change default open to enqueue and play
Instead of clear playlist and play. This was confusing people,
apparently. I should have changed it sooner, since this is what
I use normally anyway.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-06-30 22:14:43 -07:00
Christopher Snowhill
26fa9496de Add fade global shortcut
Defaulting to ctrl-cmd-O, though I may change this if
someone has any better ideas for a default.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-06-30 22:13:44 -07:00
Christopher Snowhill
696f060ffc Touched by Xcode
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-06-30 22:10:31 -07:00
Christopher Snowhill
a4f7fa68db Change "Spam" to "Copy now playing"
Change the not-obviously-named shortcut to something more
obviously named after its purpose.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-06-30 22:09:33 -07:00
Christopher Snowhill
ad9af1640c
Remove a saucy option
Nobody was likely to be using it, much less even know what it was.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-06-13 22:43:40 -07:00
Christopher Snowhill
a1caefa014
Updated BASS and company
Also included missing BASS_MPC, which was still being imported
even though it wasn't actually included.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-06-12 17:27:56 -07:00
Christopher Snowhill
42b91c6f9d Update MASShortcut for Xcode 15
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-06-08 04:40:24 -07:00
Christopher Snowhill
da2c2eb2a8 Touched by Xcode
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-06-08 04:30:25 -07:00
Christopher Snowhill
2dbe524f20
Organya: Fix deployment target for 10.13
Oops, it was somehow still set to 13.0, from when I created it.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-06-08 04:13:21 -07:00
Christopher Snowhill
eb26ab8be4
Update projects and source in prep for Xcode 15
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-06-08 04:12:29 -07:00
Christopher Snowhill
8443464b54
About Dialog: Move logo to asset catalog
Also convert it to a supported HEIF format, making it much smaller.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-06-08 03:44:24 -07:00
Shoh Sewell
f64ec07b43
Volume slider changes
Makes volume slider logarithmic when limited to 100% to allow easier changing of volume towards the bottom of the slider.
The tooltip remains as the slider location instead of the logarithmic value of the actual volume.
2023-06-08 03:40:04 -07:00
Christopher Snowhill
1cd3a3230c
File Tree: Reduce monitoring to limited changes
Only track new, removed, or renamed files. Tracking Xattr changes was
apparently causing the tracker dialog to crash on things like the user
changing file type associations, which either added or removed a per-
file association xattr, or when clicking Change All, removed this xattr
from all associated files tracked by Spotlight.

Fixes #361

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-06-04 01:37:31 -07:00
Christopher Snowhill
6c0db041e3
Playlist: Add a workaround for AppleStript URLs
AppleScript is apparently such a legacy system, that when it sends URLs
to your app to open, they're in the old Carbon format. So we need to
translate these to proper URL strings for the rest of the app to deal
with them at all.

The format of these URLs is as follows:

/method/::

Followed optionally by:

username/password@

Where the slash and password are optional.

Followed by:

hostname

Followed optionally by:

/portnumber

And finally, followed by:

:path:on:server:filename.ext

So, in hostname field, we must swap slashes to colons. And in the path
field, swap colons to slashes. What a bizarre world.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-06-01 21:34:16 -07:00
Christopher Snowhill
22cdf0aa4f
HTTP Input: Do not hang if transfer completes
If transfer completes quickly, do not hang waiting for it to achieve
reading state. Also add some comments indicating what we're doing.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-06-01 21:29:37 -07:00
Christopher Snowhill
10f22d137c
Metadata: Fixes metadata reading
metadataBlob may be null, so create dictionary in that case.

Fixes #360

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-06-01 15:15:52 -07:00
Christopher Snowhill
877660b4cc
Update feed updater script for R2
Update to use rclone with out of tree config to upload

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-06-01 00:14:48 -07:00
C.W. Betts
fa373b9ff1
Move most image assets to Xcode assets.
AboutCog.jp2 isn't moved because Xcode/Xcode assets doesn't recognize jp2 files as images.

# Conflicts:
#	Cog.xcodeproj/project.pbxproj
2023-06-01 00:13:50 -07:00
Shoh Sewell
55cb51c330
Adds decimal digits to volume slider tooltip (#356)
* Adds decimal digits to volume slider tooltip

Modifies the volume slider tooltip so that:
-If the volume slider falls below 10%, the volume tooltip will display one decimal digit of precision (e.g. 3.4%).
-Else if the volume slider falls below 1%, display one decimal digit of precision (e.g. 0.34%).
-Otherwise display the volume slider tooltip as normal.

This helps show changes in volume between 0% and 10% where a change in volume isn't shown in the UI but is heard (especially when the "Limit volume control to 100%" option is unchecked in the "Output" Preferences submenu.

* Update VolumeSlider.m

Fix variable declaration

---------

Co-authored-by: Christopher Snowhill <chris@kode54.net>
2023-06-01 00:11:16 -07:00
Christopher Snowhill
db338f929a
Playlist: Fix merging dynamic metadata dictionary
Merge the existing metadata entry dictionary with new values, because
sometimes, the caller may pass us a dictionary with some fields missing.

Fixes stream metadata for many HTTP streams.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-05-31 22:35:06 -07:00
Christopher Snowhill
13254f25b1
HTTP: Support a stream title hack
Support a stream title hack employed by some iHeartRadio streams

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-05-31 22:33:50 -07:00
Christopher Snowhill
1115e9651d
HTTP: Support much larger metadata blocks
Support icy metaint data blocks up to 4KiB in size

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-05-31 22:33:15 -07:00
Christopher Snowhill
836147b94b
Updated VGMStream to r1843-0-gb158e812
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-05-31 22:31:44 -07:00
Christopher Snowhill
b47d2154b4
FFmpeg: Fix AAC streaming
Fix AAC streaming by adding the correct audio/aac MIME type.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-05-31 21:26:11 -07:00
Christopher Snowhill
9e54885b97
Touched by Xcode
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-05-04 18:23:39 -07:00
Christopher Snowhill
11ffdcb1c1
Updated Turkish translation to near completion
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-05-04 18:23:35 -07:00
Christopher Snowhill
4d106c40ce
Updated VGMStream to r1831-27-ge6883cbd
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-05-04 18:23:31 -07:00
Christopher Snowhill
9d3089462e
Updated libOpenMPT to version 0.7
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-05-04 18:23:25 -07:00
Christopher Snowhill
3e2286683a
Translation: Added Turkish language support
This translation is mostly complete.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-02-23 22:44:24 -08:00
Christopher Snowhill
c1e5aa21fc
Translation: Updated Spanish translation
Added the new Lyrics window strings.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-02-23 22:42:39 -08:00
Christopher Snowhill
549f426e65
Main Menu: Add new Lyrics item to strings tables
Add item to strings tables for translations of the menu item. English is
already done as the base language, awaiting Spanish at least, will await
Polish and Russian when the respective translators get to it.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-02-23 18:25:33 -08:00
Christopher Snowhill
c68b4b9585
Lyrics Window: Added localization templates
Awaiting localizations of the window name in currently supported
languages.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-02-23 18:19:05 -08:00
Christopher Snowhill
0b82160512
Lyrics Window: Touched by Xcode
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-02-23 18:19:01 -08:00
Christopher Snowhill
ed38e4f8d0
Info Window: Reorder header imports
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-02-23 17:46:22 -08:00
Christopher Snowhill
799299df3a
Lyrics: Implement Lyrics window display and hotkey
Implement Lyrics window display into main app as a popup window panel,
with a main menu hotkey.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-02-23 17:45:59 -08:00
Christopher Snowhill
57a0ea6e87
Tags: Implement unsynced lyrics in Vorbis plugin
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-02-23 17:45:05 -08:00
Christopher Snowhill
de974548de
Tags: Implement unsynced lyrics in Opus plugin
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-02-23 17:44:45 -08:00
Christopher Snowhill
b0c718003b
Tags: Implement unsynced lyrics in Flac plugin
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-02-23 17:44:19 -08:00
Christopher Snowhill
593d7d155a
Tags: Implement unsynced lyrics in FFmpeg plugin
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-02-23 17:44:01 -08:00
Christopher Snowhill
190d6959fd
Tags: Implement unsynced lyrics into TagLib plugin
Implement unsynced lyrics reading into TagLib frontend plugin.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-02-23 17:41:50 -08:00
Christopher Snowhill
790eb5508b
TagLib: Implement unsynced lyrics tag support
Implement unsynced lyrics tag reading and writing into TagLib.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-02-23 17:41:12 -08:00
Christopher Snowhill
c34d869b99
Tags: Added unsynced lyrics tag interface
Added an unsynced lyrics tag interface to the PlaylistEntry class

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-02-23 17:39:52 -08:00
Christopher Snowhill
5743652879
Update copyright year in various places
Update these things a bit for the next release.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-02-05 16:46:56 -08:00
Christopher Snowhill
4131d4eae4
Updated VGMStream to r1810-97-g408cada5
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-02-04 23:46:16 -08:00
Christopher Snowhill
9bfbeaadb5
Updated libOpenMPT to version 0.6.8
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-02-04 22:13:01 -08:00
Christopher Snowhill
26242673d1
Repair SceneKit project definition
Repair the SceneKit container definition in the Xcode project file, so
that the Visualization scene gets copied properly on project build.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-02-04 22:12:31 -08:00
Christopher Snowhill
57c2adf946
Replaced r8brain with libsoxr
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2023-02-04 22:10:22 -08:00
Christopher Snowhill
01c38c9440
Playback Controller: Fixed title bar updating
This change had several components. For one, the delay of the dispatch
was increased from 5 milliseconds to 50 milliseconds. Two, the post to
the notification center was included in the delayed dispatch, so that
retains the PlaylistEntry object. Finally, the playlistController's
currentEntry object is reassigned from the input PlaylistEntry object,
which facilitates all watchers which are observing that variable for
updates. This final step also retains self for the callback, which
should be fine, since it's a quick dispatch with a short delay.

Fixes #335

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-12-09 22:35:01 -08:00
Christopher Snowhill
72ee38ad14
Audio Player: Only wait for unstopped input
Input thread now signals when it has stopped and is about to return, in
case the input thread returns before the BufferChain dealloc function
would be waiting for it to terminate. Somehow, even though the Semaphore
is being signaled at this point, the BufferChain still ends up waiting
the default of 2.5 seconds for the signal that apparently never comes,
delaying file stoppage. This prevents the wait action entirely. Must
have been some sort of race condition.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-12-09 21:18:56 -08:00
Christopher Snowhill
6daa0de425
Audio Player: Add new method of signaling stop
This new method should cause all stops to default to immediate stoppage,
and only stops that occur after an end of track signal should indicate
to play out the entire buffer.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-12-09 21:18:47 -08:00
Christopher Snowhill
1b9f460538
Playback Controller: Remove "stopping" status use
This should not really be necessary for proper player operation any
longer, and can safely be removed.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-12-09 21:18:07 -08:00
Christopher Snowhill
1de501a64a
Sandbox: Rework several blocking actions
Several actions have been reworked to be non-blocking, as their
operation should still occur in the main thread, but should not block
the thread they are called from, as they are not required to continue
processing there.

End of secure access has also been made non-blocking, as it is usually
only called when an input is done accessing a given file or folder, so
it should be important to return quickly, as the input is likely about
to terminate, and other things are waiting for it to return.

Also remove a nested block call for the storage access, as it is within
an existing serializing block, so it shouldn't need to be nested.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-12-09 21:18:03 -08:00
Christopher Snowhill
4b37ffebee
Update script: Purge cache of manifest files
Purge the manifest files from the Bunny CDN cache before starting
the update of the site.
2022-12-05 23:16:50 -08:00
Christopher Snowhill
5e0a3308c0
Decoders: Implemented Organya decoder
Based on the C++11 code by Joel Yliluoma / bisqwit, which in turn is
based on information from NX-Engine. I have also taken the liberty of
bundling the required wavetable bank and PixTone drums. Contrary to the
documentation provided with the code, my version of dou_1006.zip, as
downloaded over a decade ago, had the wavetable bank at offset 635,816
bytes into the file, not 1,115,748 bytes. Possibly a difference of
having applied the translation patch? My copy is the original version,
so I had to use a real resource parser to locate the waveforms.

The player will obey the configured sample rate, loop count, and fade
time for synthesizers, and also obey Repeat One to play indefinitely.
The code should be quite robust to minor abuses, though I can't imagine
how well it would hold up to random bad files, other than playing
outright garbage.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-12-04 03:09:52 -08:00
Christopher Snowhill
85283b99a1
Sandbox: Fixed another outstanding sync bug
Releasing sandbox access was incorrectly synchronizing on the object,
but still running code in the calling thread. It has been updated to
match the rest of the interface, which serializes all access through the
main thread only. This should prevent the sandbox from carrying stale
handles to already-released objects.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-12-03 22:13:54 -08:00
Christopher Snowhill
63bd6b29d7
Resampler: Update r8brain-free-src to v6.2
This should improve performance significantly for downsampling.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-12-03 22:13:48 -08:00
Christopher Snowhill
fadeec1827
Fix SceneKit visualization
Must always remember to have xcode-select set to Xcode.app and not
the Command Line Tools when archiving the app myself, as otherwise
Xcode will fail to package the SceneKit objects into the resulting
bundle.
2022-11-30 01:10:39 -08:00
Christopher Snowhill
9c67eb78fc
Sandbox Broker: Fixed a potential reference crash
The code which looked up Sandbox handles for a given path had a bug
where it would re-add the handle to the in-memory cache storage even if
it already had just retrieved the current handle from the cache. I hope
this will fix the crashes which have been plaguing people adding a lot
of files to the playlist all at once from a single folder.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-11-29 00:49:27 -08:00
Christopher Snowhill
d8f0a19524
Updated VGMStream to r1800-25-g599326a3
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-11-29 00:47:14 -08:00
Christopher Snowhill
5cd5fe1c5e
Dependencies: Update libVGM release and debug
Update from 0e34925..fd7da37, with the following changes:

- C140: simplify update loop, add unbanked mode
  - It resembles MAME's code more closely now.

- OKIM6258: make more tolerant against late writes

- enforce "discrete YM3438" mode for YM3438 VGMs

- Update emu2413 to v1.5.9

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-11-29 00:27:44 -08:00
Christopher Snowhill
0c5c905af3
Update Script: Change feed upload to Bunny CDN
Change upload script to use curl to upload to Bunny. The access
key is stored in the system keychain, with confirmation on access.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-11-06 23:58:59 -08:00
Christopher Snowhill
364f13338d
Vorbis Plugin: Remove unnecessary shell script
Remove shell script that was modifying the import path of a framework,
as we are no longer using libvorbis like that.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-11-06 00:49:50 -07:00
Christopher Snowhill
c627a9fc58
Add display name and category to project
Add display name and category fields to project. Thanks, Apple, for
adding those to Xcode all of a sudden.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-11-05 00:47:53 -07:00
Christopher Snowhill
c40b25b571
Info plist: Add newly required keys
For some reason, Xcode isn't adding these now. No idea what Apple
has done to cause this.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-11-05 00:42:12 -07:00
Christopher Snowhill
66e1d67b32
Sandbox Broker: Fix hang with synchronization
Synchronize with dispatches to the main thread instead of using
synchronization primitives. This prevents the main thread from
hanging another thread as a result of other threads entering the
sync block, then dispatching a callback to the main thread, which
also tries to lock on the sync block.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-11-04 21:12:06 -07:00
Christopher Snowhill
3663140064 Core Data Store: Handle concurrency properly
All concurrency from other threads should pass through the viewContext's
performBlock or performBlockAndWait functions, and no other way. So now,
all access to Core Data is either happening on the main thread, or by
using these code blocks, all of which will wait for their access to
proceed.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-10-30 16:54:33 -07:00
Christopher Snowhill
31ddbbec29 MIDI Plugin: Fix Secret Sauce memory leaks
Needed some autoreleasepools in there.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-10-28 00:16:11 -07:00
Christopher Snowhill
7fb894d721 Touched by Xcode
Xcode updated the Preferences xib.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-10-28 00:15:37 -07:00
Christopher Snowhill
504ddcf82b Sparkle: Update API a bit
This updates the API interface calls a bit, and borrows about 20 lines
of code from WireShark.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-10-28 00:02:20 -07:00
Christopher Snowhill
02a7fe84cb MIDI Plugin: Add a little Secret Sauce
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-10-27 23:06:02 -07:00
Christopher Snowhill
b88bee3f4a Amend MIDI Audio Unit player a bit
This should fix some potential initialization errors it may have had
before, but this doesn't fix the broken Sound Canvas VA plugin. Roland
says it's supposed to be broken on macOS 12+ and/or Apple silicon
anyway, so I guess there's no dodging that.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-10-21 16:37:01 -07:00
Christopher Snowhill
0d7fd92c82 Always create new ContentViewController for volume
This is needed to re-parent the VolumeSlider window, as there is only a
single VolumeSlider, but two different VolumeButtons and their
respective NSPopover windows. So, always recreate the view on open,
which doesn't appear to have a noticeable impact on performance.

Fixes #331

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-10-20 21:28:02 -07:00
Christopher Snowhill
bc66220b36 Fix Spectrum View in toolbar customizer
It was referencing the old SpectrumView class, when it should have been
referencing the safe SpectrumViewCG class, which will software render a
single frame of spectrum data when the customizing dialog is opened.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-10-20 21:26:06 -07:00
Christopher Snowhill
b1f97f3399 Updated FFmpeg to n5.2-dev-1305-g3bd0bf76fb
Among other things, fixes CVE-2022-2566.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-10-17 03:55:39 -07:00
Christopher Snowhill
e7aec3547d Ignore unnamed audio devices on enumeration
This was crashing trying to assign a nil CFStringRef from mystery audio
devices to the NSString passed to the enumerate block function, which
was predictably crashing. Ignore such devices instead.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-10-16 15:23:06 -07:00
Christopher Snowhill
a9dc4b564c Handle external artwork with .heic extension
External artwork already supported the HEIC format, just not the correct
filename extension for the format.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-10-16 15:00:28 -07:00
Christopher Snowhill
e25cfbf22c Fix a crash with embedded cue sheet handling
Tag reading can read cue sheets as either a single NSString, or an
NSArray of NSStrings, so handle either case.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-10-16 15:00:24 -07:00
Christopher Snowhill
695a03d9e8 Updated Sparkle framework to version 2.3.0
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-10-16 00:18:57 -07:00
Christopher Snowhill
066ee806dc Only process visualizations when visible
Stop visualization processing when the host window is completely
occluded, thus reducing background CPU usage levels significantly

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-10-15 23:19:22 -07:00
2f90baf4bf Merge pull request #329 from gitter-badger/gitter-badge
Add a Gitter chat badge to README.md
2022-10-13 17:17:02 -07:00
The Gitter Badger
2ead57412b Add Gitter badge 2022-10-13 17:16:07 -07:00
Christopher Snowhill
309bd3bfef Update associated file type extensions
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-10-11 23:17:43 -07:00
Christopher Snowhill
323bc8f0df Formatting fixes
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-10-11 23:00:08 -07:00
Christopher Snowhill
ce723fd44e Better locking behavior for playlist storage
This should fix up potential locking issues with maintaining a copy of
the results set while certain other background actions may happen, such
as the player updating play counts while playing.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-10-11 22:59:46 -07:00
Christopher Snowhill
bc7331cf8c Updated libraries and libraries debug set
Updated:
- libFLAC from 1.3.3-235-g772efde6 to 1.4.1
- libvgm from 001ca75 to 0e34925
- libid3tag from 0.16.1 with patch to 0.16.2

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-10-11 22:52:41 -07:00
Christopher Snowhill
fc372ef8b4 Updated libOpenMPT to version 0.6.6
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-10-11 22:47:02 -07:00
Christopher Snowhill
55882c0380 Updated VGMStream to r1776-97-g845961bb
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-10-11 22:45:07 -07:00
Christopher Snowhill
37065adf8a Updated VGMStream to r1776-46-gc0c2c3c7
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-09-06 21:56:51 -07:00
Christopher Snowhill
514374019b Update translations
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-08-21 16:49:56 -07:00
Christopher Snowhill
3a42f8896b Updated VGMStream to r1776-16-g7e4f5dc6
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-08-21 16:17:51 -07:00
Christopher Snowhill
fadb3e9ee2 Updated libOpenMPT to version 0.6.5 final
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-08-21 15:56:54 -07:00
Christopher Snowhill
d7418c3b33 [Cog Audio] Rename Semaphore.h to CogSemaphore.h
This magically fixes the stupid header maps that were pulling the system
semaphore.h into Swift projects, when they shouldn't have been doing
that in the first place. This is the same reason that the FLAC library
has its assert.h renamed to FLAC_assert.h.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-08-05 22:18:30 -07:00
Christopher Snowhill
41a04760eb [Cog Audio] Make the Swift Vis Controller work
And this is the actual meat of getting it to work properly, the changes
the Swift code needed to actually be fully functional.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-08-05 21:38:13 -07:00
Christopher Snowhill
aed52840ca [Cog Audio] Add a Swift bridging header
This makes the Swift version of the Visualization Controller usable.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-08-05 21:36:39 -07:00
Christopher Snowhill
7e267f06cb [Cog Audio] Change a couple of imports
These imports needed to be changed so that Swift bridging didn't import
the system's semaphore.h instead of CogAudio's Semaphore.h, which is a
completely different thing.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-08-05 21:35:50 -07:00
Christopher Snowhill
e5aa4287d1 First module converted to swift, but broken 2022-08-05 17:39:19 -07:00
Christopher Snowhill
06b4fc3ccc [GME Input] Correct old comment in the code
There has been an API in GME to detect tracks ending for quite some time
now, and this just adds a little bit to the existing comment, which
previously noted that there was no way to detect if a track had ended,
which may have been true several major versions of GME long past.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-08-04 18:02:49 -07:00
Christopher Snowhill
5019c6b4f6 [GME Input] Fix decoder output sample count
The output has been assigning twice as many samples as it was supposed
to ever since commit 8d851e5bda, which
ended up generating the correct 1024 samples (2048 per GME parameter),
but assigned 2048 to the AudioChunk, which resulted in over-reading the
audio buffer, and thankfully not crashing, but instead causing an awful
sound distortion effect as random memory contents were played as PCM
audio.

Fixes #320

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-08-04 18:01:09 -07:00
Christopher Snowhill
93e3dd7aa6 [CUE Sheet Container] Allow other containers
Allow .mp3 and such to fall back to the FFmpeg container handler, in
case there are chapters in a renamed file.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-08-03 21:20:42 -07:00
Christopher Snowhill
f9c7e85e72 [MAD Decoder] Do not close source ourselves
The input isn't supposed to close its own sources, as it did not open
them itself, and they should be cleaned up automatically when they are
released to zero reference count.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-08-03 21:19:51 -07:00
Christopher Snowhill
fbe232f791 [MAD Decoder] Drop RIFF files to the next input
Let the FFmpeg decoder handle RIFF files, if they happen to be named
.mp3 and not something like .wav.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-08-03 21:19:02 -07:00
Christopher Snowhill
ea3d38bcae [Update Script] Add new JSON generator
Source is included here:

https://github.com/losnoco/sparkle-to-JSON
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-08-03 21:18:09 -07:00
Christopher Snowhill
49f88ae37f Fix update script
This fixes the update URL and parameter handling. Seems there
was an extraneous newline returned by the security command.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-31 23:38:07 -07:00
Christopher Snowhill
c2a880fa52 [AdPlug Input] Fixed seeking
Looks like I never tested this, meh.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-31 22:52:03 -07:00
Christopher Snowhill
700cb962a3 Update update_feed.rb with new parameter
New parameter for update title for the site generator.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-31 21:50:05 -07:00
Christopher Snowhill
9560edf53d Updated AdPlug with a crash fix for RAD2
Fixed RAD2 files referencing instruments not present in the
file, which caused the player to reference uninitialized memory
and usually crash.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-31 21:47:58 -07:00
Christopher Snowhill
f54b6c2c9a Updated libbinio
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-31 21:47:25 -07:00
Christopher Snowhill
3f7b375bfb [Playlist Menu] Disable actions on empty selection
These actions should not be invoked when there is no selection.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-29 01:42:45 -07:00
Christopher Snowhill
af453816a0 [Playlist Queue] Save queue state change to disk
Save queue state changes to disk, rather than leaving it for later.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-29 01:41:49 -07:00
Christopher Snowhill
daa0c3fc61 [Playlist Queue] Hopefully prevent a crash state
This removal was causing crashes for some people. It should not get this
way, however.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-29 01:41:10 -07:00
Christopher Snowhill
660cb1bab1 [SID Input] Add static initializer for residfp
The SID builder needs a static initializer, otherwise multiple instances
created simultaneously, such as during populating info on adding a lot
of tracks, will race and crash the player.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-27 21:38:22 -07:00
Christopher Snowhill
4ec2146549 [File Tree] Only free Smart Folder query if used
Only free the query if it was successfully allocated.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-27 06:35:59 -07:00
Christopher Snowhill
7cb22cfeb0 [File Tree] Ask Sandbox for access to Smart Folder
Ask for access to Smart Folder dictionary file, to read its query.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-27 06:35:55 -07:00
Christopher Snowhill
86dfe8b518 [Playlist View] Prevent assigning nil textField
Prevent somehow assigning nil textField contents, as well as the
tooltip text.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-25 19:55:45 -07:00
Christopher Snowhill
b04be78f20 [Playlist Loader] Extend deduplication to CUEs
CUEs will now deduplicate playlist entries based on their dependencies,
and prevent loading redundant tracks if you add an entire directory, or
use the option to add a directory when adding single files from it.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-25 19:35:36 -07:00
Christopher Snowhill
177f055910 [Sandbox] Add Sandbox grants to places missing it
The subdirectory parser, the CUEsheet reader, and the legacy XML
playlist reader were missing grants for Sandbox access.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-25 19:35:32 -07:00
Christopher Snowhill
9f84a8bff5 Spanish translation of new option.
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-24 23:23:02 -07:00
Christopher Snowhill
37cc0b4d30 Clarified folder add message to correct button
Clarified the button name to "Open", which is what the button actually
says, not "OK". Also used double quotes around the other button name.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-24 23:22:48 -07:00
Christopher Snowhill
8809c0d257 [Sandbox Paths] Automatically clean up old paths
Clean up redundant paths automatically, and on startup. Also refresh the
preferences dialog path list every time it is opened.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-24 23:15:10 -07:00
Christopher Snowhill
8bc94e9a48 [Playlist Loader] Deduplicate loaded items
Deduplicate loaded tracks, to prevent duplicate items when adding a
folder that happens to contain playlists or CUE sheets referencing the
very same files.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-24 23:15:06 -07:00
Christopher Snowhill
14eb923529 [Playlist Loader] Add option to load more files
Add option to load every file in a folder when opening just one or more
files in that folder.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-24 23:15:02 -07:00
Christopher Snowhill
4e24c5b829 [Playlist View] Change truncation behavior a bit
Change the truncation behavior to only truncate if the length exceeds
1024 code points.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-24 19:41:34 -07:00
Christopher Snowhill
360464ceec [Playlist View] Add Sample Rate and BPS fields
Add Sample Rate and Bits Per Sample columns, hidden by default.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-24 19:41:30 -07:00
Christopher Snowhill
9d2d29d0f4 [Sandbox Dialog] Fix removing newly added paths
Newly added paths weren't adding all of the necessary data to the list
storage to make it possible to remove them without restarting the
player. Oops.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-24 19:41:24 -07:00
Christopher Snowhill
c612994cb2 Move most large stack using buffers to the heap
This should solve most potential future stack overflows.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-24 18:41:50 -07:00
Christopher Snowhill
e330c64f43 Enable warnings to track stack overuse
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-24 18:41:45 -07:00
Christopher Snowhill
e796e23afd [Sandbox Notification] Add Spanish translation
Add the Spanish translation of the new dialog.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-23 18:38:54 -07:00
Christopher Snowhill
e2d228bbc0 [Sandbox Notice] Change single to double quotes
Change the quotes.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-23 18:38:29 -07:00
Christopher Snowhill
f62a897f7d [Libraries] Fixed OpusFile debug library
It was linking to the wrong filename for libogg.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-23 18:37:40 -07:00
Christopher Snowhill
85a18f9a3e Updated VGMStream to r1745-79-g449bb5e0
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-23 18:36:45 -07:00
Christopher Snowhill
1c04d5e664 [File Permissions] Add warning dialog
Added a warning dialog to notify the user of the purpose of the add
folder dialog that will pop up after it. Otherwise, they may get the
idea that the dialog is a glitch and should be cancelled.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-23 18:04:32 -07:00
Christopher Snowhill
35dd0d38b3 [Playlist Loader] Only ask permission for local
Only ask permission for container folders if the container has local
files, and not for purely remote files, such as stream playlists.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-23 18:04:24 -07:00
Christopher Snowhill
9c4d9ebb2e [Playlist Insert] Add a further bodge fix
I wish people would stop adding files to the playlist while there's a
search filter in place.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-21 04:16:08 -07:00
Christopher Snowhill
e14c630034 [FFmpeg Input] Buffer up to 5ms each read call
Buffer up to 5 milliseconds of audio, or at minimum 1024 samples, each
call. Also pre-allocate the buffer, rather than using a stack buffer.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-21 04:03:40 -07:00
Christopher Snowhill
28d5849505 [FFmpeg Input] Do not subtract first block length
This is an unnecessary step, and results in the offset being off by the
duration of the first pre-read block. This is incorrect.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-21 04:03:36 -07:00
Christopher Snowhill
81d31dbe58 [Inputs] Severely reduce metadata update intervals
The Vorbis, Opus, MAD MPEG, and especially the FFmpeg inputs needed to
have their metadata update intervals severely reduced, to reduce CPU
usage, especially on files with lots of tags. Interval reduced to only
once per second.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-21 04:03:33 -07:00
Christopher Snowhill
41e87e3830 [Chunk List Converter] Fix repeated initialization
Oops, this compare blunder resulted in DSD decimation breaking every
1024 samples or so, owing to block sizes, and caused ticking sounds as a
result. It would also cause HDCD decoding to break completely.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-19 23:05:59 -07:00
Christopher Snowhill
716170dcad [Chunk List Converter] Minor changes
Neither of these two changes is really important, but they do simplify
things, and the division on that one function makes the non-decimating
DSD support actually functional, as the caller expects a specific number
of samples, and that was otherwise octupling the input sample count.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-19 23:05:55 -07:00
Christopher Snowhill
533c36a745 [Audio Output] Eliminated another stack buffer
Another large stack buffer was at play here. Consolidated it into an
existing buffer that can perform double duty here, since neither place
it's used conflicts with each other.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-19 23:05:50 -07:00
Christopher Snowhill
4c4f479fb6 Add a lock around access to output PTS variable
This locking should help, but I don't know why visualization jumps
around now.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-19 07:31:20 -07:00
Christopher Snowhill
b3d10bdd4d Reconfigure default toolbar layout
Now things are a little more understandable.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-19 07:31:15 -07:00
Christopher Snowhill
2a8aba1cf2 [FLAC Decoder] Fix reading CUESHEET tags
It already supported reading the CUESHEET metadata block, but I managed
to break reading and processing CUESHEET Vorbis comments, which broke
CUE tagging, as well as files that didn't have both tags.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-19 04:53:59 -07:00
Christopher Snowhill
8bd37943aa [Playlist Pasteboard] Rewrite row pasteboard
Playlist View pasteboard copier function should only be generating URLs,
and it should verify that the entry has a valid URL to begin with.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-16 06:07:53 -07:00
Christopher Snowhill
7bf1bd85b8 [Repeat Album] Add a safety test to repeat list
In case the current track isn't part of an album, or is otherwise not
matching any albums in the playlist. Though the Album filter predicate
wasn't working for a while due to changes.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-16 06:00:09 -07:00
Christopher Snowhill
6fe7883ed2 Update all localizations
This includes the base English strings, Spanish, Polish, and Russian.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-16 05:46:05 -07:00
Christopher Snowhill
b5f6e0ec20 [Audio Output] Remove renderer from synchronizer
Remove the renderer from the synchronizer on stop, before releasing the
objects, if possible.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-15 22:56:46 -07:00
Christopher Snowhill
3f212f0cfb [Audio Output] Only unregister listener if used
Only unregister the listener if it actually has been registered, and
clear the handle upon doing so.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-15 22:56:38 -07:00
Christopher Snowhill
804e7652a8 [MAD Decoder] Don't crash on bad files
The local and seekable file scanner could crash on bad MPEG files if
they failed to decode any frames and broke due to either end of file or
other unrecoverable errors, due to a division by zero error attempting
to calculate the file bitrate. Now correctly return error state if this
occurs, bailing early.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-15 22:56:30 -07:00
Christopher Snowhill
051b86cbaf [Core Data] Add access locking
Apparently we need this to prevent Core Data from stomping on itself
when another thread accesses it.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-15 07:00:29 -07:00
Christopher Snowhill
faa546bc49 [Playlist Storage] Properly force migration
Old version users needed this, but it wasn't performed correctly.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-15 06:12:57 -07:00
Christopher Snowhill
a5e6988af6 [Dependencies] Fix libogg version number
Apparently, the autotools package uses a different versioning scheme
than the CMake build. Also rebuilt and re-versioned libvorbis and
libvorbisfile.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-15 06:11:59 -07:00
Christopher Snowhill
97707e9b8f [libid3tag] Updated to avoid crash bug
Already updated to 0.16.1, but this fixes a crash bug in 0.16.1.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-15 05:10:58 -07:00
Christopher Snowhill
647c754311 [Audio Output] Greatly improve sample rate changes
Sample rate changes will now occur on exact sample boundaries, like they
are supposed to. Also, FreeSurround accounts for its output latency.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-15 03:34:10 -07:00
Christopher Snowhill
96acc738e3 Fix the Spotlight search panel
It was previously crashing horribly on adding search results. This makes
it actually functional, and renders it using a view-based table instead.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-15 03:02:41 -07:00
Christopher Snowhill
838c0d08e8 Significantly reduce stack memory usage
Oops, there were a lot of large local buffers in use here.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-14 17:28:46 -07:00
Christopher Snowhill
193af27e7e Remove obsolete helpbook document
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-14 16:11:20 -07:00
LennyLip
b62d2237f8 ru FreeSurround pref string 2022-07-14 16:08:43 -07:00
Christopher Snowhill
64fc906f40 Widen the Updates setting dialog
Oops, forgot to deal with this.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-14 16:05:33 -07:00
Christopher Snowhill
af64f93e99 [Audio Output] Make toggling DSPs safe
The DSPs should not be deinitialized from another thread, possibly while
they are currently processing.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-14 03:35:48 -07:00
Christopher Snowhill
c708e30d8c [FreeSurround] Add configuration option to enable
It now needs translation of the new string.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-14 03:35:43 -07:00
Christopher Snowhill
b95cb59a61 [Audio Processing] Increase thread stack size
Apparently, all these new changes with FreeSurround have pushed the
default 512KB thread stack size to the limit. And I'm not even using
stack variables, really, except for maybe the autoreleasepools.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-14 03:35:38 -07:00
Christopher Snowhill
1a0ab6723a [FreeSurround] Actually make it work
Apparently, the LFE channel is not being initialized at all if bass
redirection isn't enabled, and even if it is, it's uninitialized for a
great portion of the spectrum. Clear it all on every iteration.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-14 03:35:32 -07:00
Christopher Snowhill
08e76bc9f3 [Audio Output / Debugging] Fix sample logging
Fix the sample logging function that is optionally compiled into debug
versions.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-14 03:35:27 -07:00
Christopher Snowhill
4044646280 [Audio Processing] Update for new API
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-14 03:35:21 -07:00
Christopher Snowhill
7cb0054d77 [FreeSurround] Change another variable to const
This should be const anyway, as it's not written to.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-14 03:35:15 -07:00
Christopher Snowhill
8034054d72 [FreeSurround] Further improvements
Still not working, though.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-14 03:35:09 -07:00
Christopher Snowhill
f05bf71320 [FreeSurround] Fix surround block size
The output implementation has a block size of 4096, so the class
implementation should also use that.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-14 03:34:48 -07:00
Christopher Snowhill
ad9b0df8ed [FreeSurround] The rest of the implementation
This is the code that actually needs to be added to make it process
audio. This insertion makes the whole app crash when processing audio at
all. Weirdly, simply reverting these two files makes the audio code work
again. I can't explain it.

Also, commenting out CMAudioFormatDescriptionCreate makes it work, too.
There's something weird going on with that function.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-14 03:34:41 -07:00
Christopher Snowhill
eb9d642192 [FreeSurround] Experimental implementation code
This is a working implementation of FreeSurround, but I can't get it to
work in the Cog code base, as the whole project crashes head over heels
if this code is inserted into the output chain.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-14 03:34:36 -07:00
Christopher Snowhill
34884d825a [Audio Processing] Move float32 converter
Move the Float32 converter to a different location, for any future plans
to support decoding audio files to common data for any other purpose.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-14 01:46:54 -07:00
Christopher Snowhill
1713e0df7c [FLAC Decoder] Change maximum buffer size
This should be more correct, especially considering that the library can
handle 32 bit files now.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-14 01:46:47 -07:00
Christopher Snowhill
273cdef6b9 [libFLAC] Remove debug overlay
There's a bug in the debug version of the library which does not occur
in the release build.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-14 01:46:37 -07:00
Christopher Snowhill
68d323545b [FLAC Decoder] Correctly handle zero length frames
Apparently, the decoder is capable of returning zero length frames
without having hit the end of the stream.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-14 01:46:28 -07:00
Christopher Snowhill
c4e975319a Updated the help document a bit
Oops, it's been a while since I've touched this.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-13 04:56:45 -07:00
Christopher Snowhill
2c0c77dee2 [Translations] Tweaked the Preferences dialog
Now the Preferences panels are 110 points wider, and most things are
shifted around in ways to make the current set of translations fit into
the dialogs.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-13 04:37:40 -07:00
Christopher Snowhill
9fdefbf88a [Polish translation] Fixed total time formatter
Please do not translate the token names, they are used by the code to
look up which value to insert into the string.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-13 03:52:10 -07:00
Christopher Snowhill
32dcc5725b [Polish Translation] Fix the MainMenu strings file
The MainMenu.strings file had several mistyped quotation marks, and
several strings outside of quotation marks or comments, breaking the
menu translation entirely. This makes the translation actually work.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-13 03:52:01 -07:00
Christopher Snowhill
0518f99aaf [Polish translation] Activate localized strings
Template was missing from the project declaration. Oops.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-13 03:51:53 -07:00
Christopher Snowhill
6a5fa23807 XIB touched by Xcode
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-13 03:51:47 -07:00
Christopher Snowhill
29dfe593f1 [Ogg Vorbis/Opus] Fix tag clobber on play
Fixed the tags being overwritten by an "update" on non-streaming files.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-13 00:14:17 -07:00
Christopher Snowhill
8c4f9a7123 [Ogg Vorbis/Opus] Fix picture metadata handling
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-13 00:14:11 -07:00
Christopher Snowhill
5238965534 [Playlist Insert] Add a special case for filtered
Insertions which occur when the playlist is filtered can try to add past
the end of the playlist. Let's try to dodge that.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-13 00:14:06 -07:00
Christopher Snowhill
8c0abf5fab Update several of the dependencies
- Updated libFLAC to the latest Git commit, post 1.3.4.
- Updated libid3tag to 0.16.1.
- Updated libopus to the latest Git commit.
- Updated my FFmpeg libfdk-aac patch. Previously was overwriting
  memory when it was supposed to be skipping samples.

Also added debug versions of several of the libraries, and changed
the library extractor script to unpack the debug libraries over the
release set to add the particular matching debug versions when
building a debug build.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-13 00:13:07 -07:00
LennyLip
40cc36d8df Russian translation: new strings (#310)
* Update MainMenu.strings

* Update Preferences.strings
2022-07-11 19:59:51 -07:00
Christopher Snowhill
9462e9fb70 [MAD Decoder] Fix streamed MP3s not working
Due to a change designed to stop playback when the end of the file is
reached, which should not be checked for unseekable files, which are
web streams that only stop when the connection drops, or when the user
stops playback manually.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-11 19:50:27 -07:00
Christopher Snowhill
7adaeb4dd0 [HTTP Reader] Fix reading small static files
The reader was previously returning a failure state on open if the read
completed and fit entirely into the read buffer, which broke most remote
M3U or PLS playlists.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-11 19:50:21 -07:00
Christopher Snowhill
824675ae59 [Sandbox Broker] Only pop suggester for local URLs
Only pop up the path suggester and check on local file URLs, not remote
URLs, which shouldn't be checked, since they don't require sandbox
permission grants or bookmarks.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-11 19:50:16 -07:00
Christopher Snowhill
7a3d571492 Update About Window logo
The previous version was from an old render. Also apply some minor
tweaks to the scene.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-11 19:50:12 -07:00
Christopher Snowhill
25b90f300f [Shuffle] Fix Shuffle Album mode
Oops, I should have remembered that the data structure changes would
break this search predicate. Now apply the search predicate to the
playlist representation, which allows searching against the data blocks
using the PlaylistEntry property implementation.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-11 19:50:07 -07:00
a217bbec1e Update README.md
Third's the charm.
2022-07-11 19:50:02 -07:00
8cf84e5b90 Update README.md 2022-07-11 19:49:55 -07:00
fc6d454432 Update README.md 2022-07-11 19:49:50 -07:00
ff16c583a0 Added Lokalise logo and instructions. 2022-07-11 19:49:45 -07:00
Christopher Snowhill
a92b857161 Add missing strings to new translations
Still in need of translation from their respective authors.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-11 15:26:11 -07:00
LennyLip
1ed85a1c84 Russian lang fixes 2022-07-11 15:21:41 -07:00
LennyLip
2ecf110632 Russian translation 2022-07-11 15:21:35 -07:00
Christopher Snowhill
1ad6c78d83 Fix missing Polish declarations in project
There were still missing things.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-11 15:03:35 -07:00
Christopher Snowhill
6134cc47fe Activate Polish translation
The declarations for the translation were missing from the project files
so that it wasn't being used. Also added the missing strings to the
files that were already added to the translation.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-11 14:41:15 -07:00
Christopher Snowhill
33e1086842 [Visualization Controller] Minor guard check
Guard check in case visualization controller is called before any data
is posted to it.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-11 14:41:09 -07:00
Christopher Snowhill
e6908ac945 [Headphone Filter] Minor changes
Change a variable type, to avoid a warning.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-11 14:41:02 -07:00
Christopher Snowhill
833e298d3d [Audio Converter] Minor change for format changes
This should also seal up any potential hole for problems if there's an
audio format change and no audio buffered.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-10 16:36:43 -07:00
Christopher Snowhill
ac9e404b23 [Audio API] Repair the damage to the input chain
The input chain could hang up indefinitely, and MAD decoder didn't
indicate end of file properly.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-10 16:24:08 -07:00
pax
36040582ce first(really basic) open url panel translation. 2022-07-10 15:41:42 -07:00
pax
5e09d8f4ef translated equalizer strings, as well as the spotlight thing. 2022-07-10 15:41:34 -07:00
Christopher Snowhill
8d851e5bda [Input API] Change input readAudio method
readAudio now returns an AudioChunk object directly, and all inputs have
been changed to accomodate this. Also, input and converter processing
have been altered to better work with this.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-10 15:22:04 -07:00
Christopher Snowhill
c43ebba424 [Downmixer] Only downmix to stereo if not stereo
When downmixing to mono, only downmix to stereo first if the source is
not already stereo.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-10 15:21:57 -07:00
Christopher Snowhill
c32d14a048 [Project Files] Change most to enable modules
Most projects needed to be changed to enable C or Objective C modules.
Hopefully, this improves debugging.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-10 15:21:48 -07:00
Christopher Snowhill
cbc1c85a71 [Cuesheet Input] Don't repeatedly open file
The input file has already been opened for decoding by an earlier step
in the testing process, reuse the decoder from that. Spares a decoder
open cycle on all embedded cuesheet supporting formats.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-10 15:21:42 -07:00
Christopher Snowhill
f150065194 [HRTF] Force filtering of odd channel formats
Apparently, Apple's Spatial Audio processor doesn't really support weird
configurations like this. So we need to filter them down to stereo.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-09 18:42:03 -07:00
Christopher Snowhill
1c6db85555 [HRTF] Reverse Z axis of speakers above listener
Apparently, positive elevation is above, negative is below.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-09 18:41:07 -07:00
Christopher Snowhill
54238d29e4 Update some strings from Polish translation
Update missing strings, including one which was translated already in
the comments.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-09 16:24:39 -07:00
02f488c7bf [Spanish Translation] New strings
Signed-off-by: Kevin López Brante <kevin@kddlb.cl>
2022-07-09 16:24:26 -07:00
c735c8f387 [Translation] Privacy policy URL can now be loaded from strings files
Signed-off-by: Kevin López Brante <kevin@kddlb.cl>
2022-07-09 16:24:16 -07:00
Christopher Snowhill
2251650b6e Add two missing strings from the Sparkle branch
Oops, those were missing.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-09 16:24:05 -07:00
079301025c [Spanish Translation] Updated string for new HRTF filter. 2022-07-09 16:23:54 -07:00
Christopher Snowhill
f1381b11fd Implemented all new HRTF filter
This filter replaces the old one, and uses OpenAL Soft presets. Since
there aren't that many of those, I've left off configuration for now,
except to turn it on or off.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-09 16:23:43 -07:00
pax
b0ec50e96e The Polish translation is mostly ready. 2022-07-09 16:00:12 -07:00
pax
7c3500a925 *added Polish translation, not fully ready, but there ya go. 2022-07-09 16:00:04 -07:00
Christopher Snowhill
413ec69ee4 Truncate text in playlist to a reasonable length
1024 characters aught to be enough for any playlist view.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-08 16:36:11 -07:00
Christopher Snowhill
ef0dd921ab Remove the meta string cache
It wasn't helping anyway.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-08 16:36:01 -07:00
375eae4be2 [Spanish Translation] Added strings for new Info Inspector fields 2022-07-08 16:35:52 -07:00
Christopher Snowhill
1d3bc8045c Ditch the data compression
It just wasn't working out.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-08 16:35:44 -07:00
Christopher Snowhill
8ee4a04f3b Experimental tag support redesign
This redesign completely changes how tags are stored in memory. Now all
arbitrary tag names are supported, where possible. Some extra work will
be needed to support arbitrary tags with TagLib, such as replacing it
with a different library.

Translation pending for a couple of strings.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-08 16:35:34 -07:00
Christopher Snowhill
44e1fc5c49 [Playlist] Increase default font size to Regular
Regular control size ends up being 13 points, rather than the previous
default of Small control size, which ended up being 11 points.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-07 17:56:25 -07:00
Christopher Snowhill
7d8c2c53a0 Updated VGMStream to r1745-58-g828c6b09
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-07 16:44:15 -07:00
Christopher Snowhill
3a16a53bd1 [Path Suggester] Only process local file URLs
Playlist entries for non-file URLs should be ignored.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-07 16:38:23 -07:00
Christopher Snowhill
812da2e331 [Table Views] Add a safety check to cell creation
Cell creation may create some other type of view, somehow. No idea how.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-07 16:37:42 -07:00
Christopher Snowhill
3d1be2ca0d [File Tree] Significantly improve the watcher
- Switch to fine grained folder and file watching responses
- Navigate the PathNode tree using a fast dictionary of path components
- Quickly refresh the file tree by locating parent nodes to refresh

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-06 22:41:38 -07:00
Christopher Snowhill
a0621b2537 [File Association] Correctly play files on open
When opening files from external association, such as opening files, or
opening folders with Cog, correctly obey the configured clear and play
or enqueue and play actions, by playing the new additions.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-06 17:04:02 -07:00
Christopher Snowhill
38beb9e930 [Playlist Loader] Fix Clear and Play action
Clear and Play was broken by the previous update. This fixes it.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-06 16:04:17 -07:00
Christopher Snowhill
0732b176fd [CI Scripts] Update to use command -v
Instead of `which`.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-06 14:44:05 -07:00
Christopher Snowhill
7e516f8cfe [Playlist Loader] Load files in the background
Load new playlist entries in the background, asynchronously.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-06 14:28:34 -07:00
Christopher Snowhill
36d8fa5ba5 Attempt for one last time to fix Xcode Cloud
This should hammer fix it. That'll show them for forcing a shallow
commit on me.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-06 04:38:54 -07:00
Christopher Snowhill
6984ce326c Attempt to fix Xcode Cloud again
This time, run the git fetch tags and genversion in the pre
xcodebuild script.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-06 04:23:15 -07:00
Christopher Snowhill
4956569206 Fix Xcode Cloud CI script to fetch tags
The script needs to fetch repository tags to function properly.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-06 04:13:30 -07:00
Christopher Snowhill
0c6e69015f Change Crashlytics symbol upload script
Script should not fail an otherwise successful build.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-06 03:37:52 -07:00
Christopher Snowhill
adc159eb05 [Playback] Prevent erroneous file from repeating
Prevent Repeat Single from locking up the player on an unplayable file.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-06 02:20:20 -07:00
Christopher Snowhill
8f4fb4a44c [Sandbox Broker] Greatly speed up path resolving
The fragment remover need not detect whether the given path is a folder
or a file, as it is only removing hash marked fragments, not actually
removing the entire filename if it's only a file and not a folder like
the old versions used to. This greatly speeds up access, especially on
network shares.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-06 00:04:28 -07:00
Christopher Snowhill
9bf5bbba80 [Path Config] Allow multiple selection
This more easily allows removing multiple paths at once.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-06 00:03:07 -07:00
Christopher Snowhill
aa7eb52231 [Path Config] Remove exact items by token id
Should use the exact token object to remove them, rather than doing a
path comparison.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-06 00:02:34 -07:00
Christopher Snowhill
1fb636ffd2 [Path Config] Properly prune the database
When cleaning up the path list, actually remove the pruned entries from
the Core Data storage, so they don't end up resolving to broken
bookmarks in the player, breaking playback on migrated configurations.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-05 23:47:47 -07:00
Christopher Snowhill
933b06d5dd [Path Config] Properly report broken bookmarks
Broken bookmarks weren't reporting as isStale, but rather, were failing
to resolve at all, and without this change, they were impossible to
detect in a migrated configuration.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-05 23:47:41 -07:00
Christopher Snowhill
fceee35896 [Album Art] macOS Ventura natively supports AVIF
Disable the compiled in AVIF support there, as the OS supports it
natively. Keep the libraries for older OSes.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-05 23:47:34 -07:00
Christopher Snowhill
3958af0670 [FFmpeg Decoder] Further improve Matroska tags
Matroska defaults the date field to "date_recorded".

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-05 18:15:34 -07:00
Christopher Snowhill
de72631ea5 [FFmpeg Decoder] Better handle Matroska tags
Matroska files use the "TITLE" field for the album when there are
chapters. Also, Matroska container uses shorter gain field names for
album and track gain, differentiating them by either being global or
specific to each chapter.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-05 17:41:18 -07:00
Christopher Snowhill
a474b469fa [FFmpeg Decoder] Enable Metroska and WebM videos
Enable playback of video file extensions. Like other video formats
handled by the FFmpeg decoder, video streams are dropped in decode and
only the first audio stream is played.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-05 14:26:04 -07:00
ebe301a9b8 [Spanish Translation] Updated strings in Appearance preferences
Signed-off-by: Kevin López Brante <kevin@kddlb.cl>
2022-07-05 13:54:57 -07:00
Christopher Snowhill
aba75e2184 [FLAC Decoder] Safety decoding for tag reader
Use tag string encoding guessing for tag decoding, just in case there
are invalid files with non-standard encoded strings inside the tags, or
if there are streams with such tags. We don't want any crashes.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-05 13:54:45 -07:00
Christopher Snowhill
c3ca29db0d [Spectrum] Enable switching style at runtime
It is now possible to switch the display style at runtime, while the
views are open.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-05 13:54:35 -07:00
Christopher Snowhill
b0d1533b43 Interface builder touched by Xcode
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-05 13:54:24 -07:00
Christopher Snowhill
c4790af7c0 Reformat spaces to tabs
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-05 13:54:17 -07:00
Dzmitry Neviadomski
81c98736fa Add preference to choose between SceneKit and DDB spectrum. 2022-07-05 13:54:10 -07:00
Christopher Snowhill
cd45941a93 Greatly improve tag reading performance
Improve tag reading performance for Ogg, Opus, FLAC, TTA, and TAK, by
eliminating TagLib from the equation in those cases and just using the
respective file inputs to do the tag reading, which is apparently a lot
faster anyway.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-05 00:55:21 -07:00
Christopher Snowhill
a1ea668a41 [Audio Player] Eliminate an avenue of lockups
Prevent the player from locking up in certain circumstances, by not
locking chainQueue the entire time this function is processing.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-05 00:53:55 -07:00
Christopher Snowhill
e37d1d15b1 Remove unnecessary files from build and copy steps
Remove a single .inc include from CogAudio build phase, as it's included
but not compiled as Pascal like Xcode thinks. Also remove a bunch of
files from being copied into the resulting .framework and .bundle files
during link stage, as we don't need to distribute that stuff.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-04 23:58:58 -07:00
Christopher Snowhill
511f1a1937 [Playlist Loader] Revert background loader
This reverts most of 802a86a3d8, since it
didn't work anyway.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-04 18:08:45 -07:00
Christopher Snowhill
92bce537d1 [Playlist Loader] Fix background queue post action
Post action now returns the files.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-04 17:50:29 -07:00
Christopher Snowhill
aeee143de2 [Equalizer] Fix support for arbitrary channels
The deinterleaved format was being specified incorrectly. Now it asks
for the correct format, which is deinterleaved, and the bytes per frame
or packet sizes are relative to a single channel's buffer, not all
buffers. Oops, that could have been more clear in the documentation.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-03 22:39:43 -07:00
Christopher Snowhill
9e66838b9b [Equalizer] Remove unnecessary code
This code is obsolete, remove it.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-03 22:37:52 -07:00
Christopher Snowhill
740613b95a [Sandbox] Ask for permission for container folders
Ask for permission to access the folders containing container files,
such as .CUE sheets, or .M3U or .PLS playlists.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-03 22:37:11 -07:00
Christopher Snowhill
969bf4f502 [M3U Playlist] Reformulate safety checks
Apparently someone managed to crash this with their playlists. No idea
how. Added more safety checks.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-03 14:32:58 -07:00
Christopher Snowhill
6a46389310 [Tag Reading] Moved external cover art reader
Moved external cover art reader to a place where it can be used for any
format, even formats unsupported by Metadata Reader interfaces.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-02 14:59:19 -07:00
Christopher Snowhill
e41f4e8556 Update Info.plist.template
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-02 04:38:59 -07:00
Christopher Snowhill
4450f13a8e [Plugin Loader] Unregister loader callback
This callback should be unregistered when plugin loading completes,
otherwise we could end up processing bundles loaded by external stuff,
like Audio Units loading for MIDI playback.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-02 04:38:51 -07:00
Christopher Snowhill
310a6d44f9 [FFmpeg Input] Add .m4b and .m4r extensions
Add support for more file name extensions, so we don't fall back on
Core Audio Input for these files.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-02 04:38:43 -07:00
Christopher Snowhill
e9f580cfbc [FFmpeg Input] Implement SoundCheck tag support
Implement support for the Sound Check tag format.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-02 04:38:35 -07:00
Christopher Snowhill
bf1afd1923 [TagLib] Disable MP4 tag reader, as it can crash
This MP4 tag reader is buggy. Disable it in favor of FFmpeg decoder's
metadata reader.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-02 04:38:29 -07:00
Christopher Snowhill
fce21785c2 [MIDI Input] No longer crash when seeking to the end
When seeking to the end of a file, no longer crash due to out
of range std::vector access, because it was using at() with an
offset of the array size. Instead, offset from the begin()
iterator return value, which allows offsetting to end().

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-02 04:38:23 -07:00
baec7ed72d [Spanish Translation] Added new strings for synthesis settings
Signed-off-by: Kevin López Brante <kevin@kddlb.cl>
2022-07-02 01:27:06 -07:00
Christopher Snowhill
5d7a9798fe [Synthesizers] Implement default overrides
Default time, fade, loop count, and sample rate may now be overridden.

Synchronized preferences strings tables. Spanish translation of new
options pending, new releases won't be pushed until they're complete.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-02 01:27:01 -07:00
Christopher Snowhill
33a24d4d3e [Translation] Widen the Path Suggester column
Widen the enable column, for the Spanish description of the column
header.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-01 14:30:37 -07:00
Christopher Snowhill
18a8baf93b [Translation] Fix translation of Path Suggester
Also fix the fact that the XIB wasn't embedding the XIB/NIB in the app
as a result of the translation move.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-01 14:29:54 -07:00
Christopher Snowhill
01ef37c565 [Audio Output] Fix equalizer support
Equalizer was copying the output of the equalizer repeatedly to the
first output channel, instead of copying each channel correctly. This
had the effect of making the equalizer output adjusted audio to only the
left channel in stereo output, and possibly render the stream sounding
weird.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-01 14:11:27 -07:00
Christopher Snowhill
a4f2664ca4 [Plugin Controller] Add Cue sheet safety check
If somehow a plugin doesn't load, skip cuesheet should skip it anyway,
as we don't want any recursive loops.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-01 13:38:49 -07:00
Christopher Snowhill
d9f111d735 [Audio Output] Remove unnecessary variables
These variables weren't being used anyway, so remove them.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-01 12:43:25 -07:00
b701fa712e [Translation] The Big Translation Commit
- Plugs the Total duration text to macOS's localization technology
- Adds a proper Spanish translation
- Adapts certain dialogs to make them more suitable for translation

Signed-off-by: Kevin López Brante <kevin@kddlb.cl>
2022-07-01 12:43:18 -07:00
Christopher Snowhill
1b0e765d38 [libOpenMPT] Remove unnecessary compile option
ENABLE_ASM isn't even used anywhere in the library any more.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-01 01:02:17 -07:00
Christopher Snowhill
59bf8c6cf9 Move all dialog XIBs for translation
Make it easier to translate the relevant dialogs now.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-01 00:39:37 -07:00
Christopher Snowhill
d4b440d6a5 Updated credits file with Patron preference
Oops, missed that Patreon message, because Patreon did not email me.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-01 00:17:01 -07:00
Christopher Snowhill
9e02492dc2 Update Credits.html in Spanish placeholder
Spanish translation really needs doing some time soon, maybe.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-07-01 00:12:56 -07:00
Christopher Snowhill
da1973bcd9 Build libOpenMPT from source once again
Bundle libOpenMPT as a dynamic framework, which should be safe once
again, now that there is only one version to bundle. Also, now it is
using the versions of libvorbisfile and libmpg123 that are bundled with
the player, instead of compiling minimp3 and stbvorbis.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-30 22:56:52 -07:00
Christopher Snowhill
8b8fbad6d9 Updated libOpenMPT to version 0.6.5-pre.1+r17609
This allows us to eliminate the requirement to continue bundling version
0.5.x of libOpenMPT for compatibility with macOS 10.13 through 10.14.x.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-30 21:28:44 -07:00
Christopher Snowhill
099588b7bd Restore the File Tree, now with a chooser button
Revert "Remove the file tree, as Sandbox does not permit"

This reverts commit 35400e1320.

This also changes how the File Tree choosing works.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-30 16:59:35 -07:00
Christopher Snowhill
73c4360b1d [Info Inspector] Improve formatting of sample rate
Sample rate now has a locale independent formatting, and no longer uses
scientific notation for large numbers.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-30 00:28:30 -07:00
Christopher Snowhill
da21cd7341 [Play Counts] Fix reporting play counts
Play counts are guaranteed to be reported on the correct track now.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-29 23:28:11 -07:00
Christopher Snowhill
27478e5df2 Update libVGM and BASSMIDI, SF3 support
BASSMIDI now includes SF3 support, as well as several other changes.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-29 23:27:28 -07:00
Christopher Snowhill
d739e68e8e [Sandbox] Synchronize write accesses to storage
Synchronize writing to the bookmark storage to the main thread.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-29 19:41:03 -07:00
Christopher Snowhill
4ce180fb2a [Sandbox] Fix URL fragment removal function
This should be deleting from the #, including the #.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-29 19:39:47 -07:00
Christopher Snowhill
3c0ccd9d46 [Context Menu] Hook up Reset Play Counts item
Actually hook up the Reset Play Counts menu item so it actually does
something instead of just sitting there looking pretty.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-29 12:42:12 -07:00
Christopher Snowhill
b24b9744c1 [Sandbox Config] Correctly test paths for files
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-29 12:11:01 -07:00
Christopher Snowhill
61778b7165 [Sandbox] Remove startup folder consent prompt
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-29 12:03:18 -07:00
Christopher Snowhill
7d26150c26 [Sandbox] Support bookmarking individual files
Individually added files, directly opened by the user, may now store
bookmarks in settings.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-29 12:00:25 -07:00
Christopher Snowhill
35400e1320 Remove the file tree, as Sandbox does not permit
The Sandbox does not permit such controls to exist.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-29 12:00:12 -07:00
3d94978f82 [Info Inspector] Made fields selectable, and fixed blending issue with
album art

Signed-off-by: Kevin López Brante <kevin@kddlb.cl>
2022-06-29 11:57:32 -07:00
fe7c424843 [About Window] Fixed appearance for systems without Dark Mode
Signed-off-by: Kevin López Brante <kevin@kddlb.cl>
2022-06-29 11:57:26 -07:00
Christopher Snowhill
1a4c140708 [Sandbox] Handle file tree path config better
Handle the configuration better, by adding the path to the grants list
if it is newly configured.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-29 00:31:54 -07:00
Christopher Snowhill
29c070a616 [Sandbox] Automatically save folder bookmarks
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-28 23:15:08 -07:00
Christopher Snowhill
8b7418857d [Sandbox] Reduce entitlements granted by default
Since App Store approval decided these suddenly matter.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-28 23:14:53 -07:00
Christopher Snowhill
a35459719d [Sandbox] Show grant dialog on launch if empty
If there are no configured paths, show the grant page on every startup.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-28 23:14:21 -07:00
Christopher Snowhill
802a86a3d8 [Playlist Loading] Process messages while loading
Process main queue messages by handling the loading in a background
queue, and sync it to the main thread periodically, while pausing to
wait for the results. This allows the file open dialog to return
immediately, and display loading progress on the status bar.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-28 20:28:10 -07:00
Christopher Snowhill
f8d2837c4e [Playlist View] Change ratings column to variable
The ratings column needs to be made variable width, for variable font
sizes. If anyone knows how to force the width to fit the current text,
I'm open to suggestions.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-28 20:27:57 -07:00
Christopher Snowhill
112366c850 [Crash Handling] Enable exceptions for debugging
Debug builds should have exceptions enabled, rather than crashing.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-28 20:27:43 -07:00
Christopher Snowhill
a1a8607a84 [About Dialog] Add needed WebKit framework
This is needed for macOS older than 11.0? 10.15? to open the About
dialog without crashing.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-28 04:26:55 -07:00
Christopher Snowhill
690153f561 [Play Info] Implement track rating system
The track ratings are stored in the same stats table as the play counts.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-28 01:55:30 -07:00
Christopher Snowhill
b33e3ff6b3 [Audio Output] Restart correct track
When restarting playback on the current track, restart the correct
track, in case restarting near the end of it.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-27 22:03:02 -07:00
Christopher Snowhill
bedfac4e33 [Play Counts] Add option to (mass) reset counts
Add option to reset counts for all selected tracks on the playlist.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-27 21:50:14 -07:00
Christopher Snowhill
66102a6cda [Play Counts] Commit play count edits to storage
Was calling commitEditing rather than commitPersistentStore, whoops.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-27 21:49:32 -07:00
Christopher Snowhill
b36ebfe740 Resource templates touched by Xcode
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-27 21:48:43 -07:00
Christopher Snowhill
2ed78a0639 [Play Counts] Track play counts of correct track
Track play counts for the correct track, even on short tracks. Also
correctly track the play count of the last played item in the play queue
which stops with bufferChain set to nil, so the previous iteration was
not tracking it.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-27 21:46:36 -07:00
Christopher Snowhill
ae019409c5 [File Tree] Pop permission grant on setting root
Setting the root path should now pop up a permission grant dialog.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-27 16:17:19 -07:00
Christopher Snowhill
66262c2a71 [Sandbox Broker] Synchronize full access operation
Full access should be synchronized, otherwise rapid access to the same
path from different threads will cause crashes.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-27 01:00:11 -07:00
Christopher Snowhill
f567750d56 [Metadata Cache] Actually run cleanup thread
Previously, the cleanup thread was not being run. Also, only reset the
metadata deduplication store when the cache is first emptied.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-27 00:58:56 -07:00
Christopher Snowhill
2a99bb076f [Audio Output] Change converter back to Obj-C
Change converter source file back from Objective-C++ to Objective-C.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-27 00:34:19 -07:00
Christopher Snowhill
dd65665990 [Sandbox Broker] Bypass entitled paths
Include entitlement granted user folders in the permission check, so
that if the file or folder is nested under one of them, it allocates a
static permission object, rather than querying the list of configured
paths every time. This also prevents the player from popping open the
path grant / suggester dialog every time a default path is in the file
set listed, which should provide some relief to most users.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-26 23:52:38 -07:00
Christopher Snowhill
c477fbf553 Attempt to fix Xcode Cloud CI
Try to generate the Info.plist before xcodebuild runs.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-26 22:59:31 -07:00
Christopher Snowhill
03b3b43cfe Update versioning setup
Versioning now happens before building Cog itself, and goes
into the Info.plist in the project directory. The original
file became a template file which is altered any time a
build occurs.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-26 22:08:42 -07:00
Christopher Snowhill
9d8e278a57 Update debug.yml
Switch to building on macos-12
2022-06-26 22:07:12 -07:00
Christopher Snowhill
ed9e352543 Add Package.resolved back to repo
Guess we can't use Github actions now, because this file breaks those.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-26 20:25:41 -07:00
Christopher Snowhill
16fdc1de6a Add CI scripts for Xcode Cloud
Add a post clone script for Xcode Cloud

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-26 20:22:54 -07:00
Christopher Snowhill
fc37e96099 Automatically unpack libraries before building
This required adding the included script in every project that links to
one of the bundled libraries. The script is designed to sleep for a
while if another thread is already extracting the libraries. The script
uses a temporary file as an extraction step lock, so other instances
sleep, and then detect the libraries.updated file, which is created
before the lock is removed.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-26 20:11:52 -07:00
Christopher Snowhill
03a2c0c16e Updated VGMStream to r1745-47-gfa55119d
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-26 15:11:23 -07:00
Christopher Snowhill
206a3e42e7 [Equalizer] Prevent crash on stop
Wait for the equalizer to be shut down properly by the main thread
before destroying it. Otherwise, the main thread could crash on stop,
due to accessing the equalizer handle while it's being torn down in the
output thread.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-26 14:47:34 -07:00
Christopher Snowhill
6f6b5d6986 [Visualization System] Change API a bit
Now the API makes both PCM and FFT data optional, and will do nothing if
neither are requested. Also, it now supports a latency offset in seconds
with floating point precision. The two built-in visualizations currently
request zero larency. Increasing the latency asks for even older samples
while specifying a negative count requests samples from the "future"
relative to what the listener is hearing.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-26 05:39:24 -07:00
Christopher Snowhill
038b0b8067 [Play Events] Don't bug on end of playlist
Don't bug out on end of playlist, when didBeginStream will receive a nil
track pointer, which should result in unsetting the current track in the
player, and not send a DidBegin notification to everything, including
the visualization views' event handlers.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-26 05:28:43 -07:00
Christopher Snowhill
a57827f4da [Play Counts] Fix counts for tracks with subsongs
Fix counts for tracks with subsongs from piling all the counts onto the
first subsong seen, by using the URL fragment in the filename check and
storage.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-26 04:37:41 -07:00
Christopher Snowhill
39be3ab962 [Audio Output] Fix for previous commit
This fixes the problem caused by the following commit:

050aaaf852

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-26 03:57:19 -07:00
Christopher Snowhill
17df6cde4f Fix compilation
Oops, that last suggester change broke compilation.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-26 03:08:20 -07:00
Christopher Snowhill
2945de085d [Sandbox] Don't try to grant access to container
Do not try to grant access to the app's container folder when searching
for paths to add.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-26 02:58:25 -07:00
Christopher Snowhill
c2ef7d0e61 [Sandbox] Compare to the actual user paths
Remove the sandbox reference, because the user will add folders outside
the sandbox, and we have entitlements to access these folders.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-26 02:56:44 -07:00
Christopher Snowhill
96a7255779 [Sandbox] Suggest URLs that are contained in CUEs
Cuesheets can now expose which URLs they contain, which may help with
sandbox path configuration. That is, if the CUE sheets are already
readable.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-26 02:54:19 -07:00
Christopher Snowhill
050aaaf852 [Visualization] Resample more audio if present
If upsampling the audio by a significant factor, it may be necessary to
process more than one buffer at a time, rather than lose input.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-26 01:09:55 -07:00
Christopher Snowhill
5f52a4be81 [Audio Output] Better handle latency oddities
The visualization buffer now holds up to 45 seconds of loop, and the
latency measurement code now caps this at 30 seconds, and restarts the
output if latency exceeds 30 seconds, such as if a sound output is
reset.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-26 01:09:01 -07:00
Christopher Snowhill
5f2335b796 [Audio Output] Play last track and stop correctly
Play last track up until it actually ends, and stop on command.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-25 06:42:56 -07:00
Christopher Snowhill
d33475953e [FFmpeg Decoder] Don't post redundant meta event
Don't post a metadata event on open, because inputs will relay it to the
player as an early notification bubble, which is unwanted.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-25 06:05:03 -07:00
Christopher Snowhill
7a56447271 [Audio Output] Fix serious memory leakage
For one thing, the example code I followed was Swift and handled auto
releasing handles in the background, while Objective-C requires manual
handle reference management.

For two, there was no autoreleasepool around the block handling the
input audio chunks, which need to be released as they are pulled out and
disposed of. This also contributed to memory leakage.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-25 06:00:11 -07:00
Christopher Snowhill
b86ec3340f [fdkaac] Update libfdk-aac to 2.0.2 with patches
Update fdk-aac library in dependencies package.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-25 05:13:22 -07:00
Christopher Snowhill
36c82a61e7 [Audio Output] Fix serious deadlock issue
There was a serious deadlock issue. Now it is fixed. Whew.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-25 05:12:43 -07:00
Christopher Snowhill
ab13b66755 [InputNode] Syntax code fix
This code was misformatted.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-25 05:10:33 -07:00
Christopher Snowhill
86de03a1ab [Play Count Info] Tabulate first seen info later
Tabulate first seen information when loading the metadata, rather than
when first adding the tracks to the playlist. This should fix first seen
information when metadata is available, as the information will be
useless without track titles.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-25 02:40:05 -07:00
Christopher Snowhill
0f923e6072 [Cuesheet] Greatly improve loading performance
Cuesheets were invoking a seek operation on open, rather than on first
playback, and this has a heavy toll on FFmpeg audio formats, apparently.
Defer the initial seek to the first readAudio call, and do not invoke it
if a seek was already called on that input session.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-25 02:38:17 -07:00
Christopher Snowhill
cb2ce5675a [FFmpeg] Fix chapter handling and seeking
Fix chapter startup, and chapter seeking.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-25 02:36:14 -07:00
Christopher Snowhill
1f56e5ef5a [FFmpeg] Seek including skip samples
This is essential for chapters, as otherwise, we would be skipping an
awful lot of samples every chapter, or every seek within a chapter.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-25 01:43:36 -07:00
Christopher Snowhill
2663b5007d [FFmpeg] Support files with chapters
Support file chapters, including metadata reading for each chapter.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-25 01:35:07 -07:00
Christopher Snowhill
72572c9c7f [FFmpeg] Deduce the length from the container
Determine the length of the file from the container, rather than the
individual audio stream. The former is more likely to be set than the
latter is.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-25 00:05:57 -07:00
Christopher Snowhill
3de7a34eb8 [FFmpeg] Update FFmpeg library and decoder plugin
Update based on newest changes from upstream.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-24 23:51:12 -07:00
Christopher Snowhill
86d8f04966 [Audio Output] Correctly configure WAVE layouts
Correctly configure AVFoundation with the channel layouts supported by
WAVEFORMATEXTENSIBLE speaker position flags, which includes varied
formats supported by FFmpeg and Core Audio inputs.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-24 23:29:32 -07:00
Christopher Snowhill
b55955ef1c [Audio Output] Correctly delay layout updates
Channel layout updates should be delayed when resampling, just like
sample format changes are.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-24 23:29:18 -07:00
Christopher Snowhill
8e1175bbd4 [FFmpeg] Update minimum platform for x86_64
Update minimum platform version to macOS 10.13.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-24 22:33:50 -07:00
Christopher Snowhill
62e2880b49 [FFmpeg] Enable TrueHD decoder and demuxer
Oops, somehow I didn't enable TrueHD support.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-24 22:22:29 -07:00
Christopher Snowhill
1ac3e5cd22 [FFmpeg] Update libfdk-aac fixed point patch
Update this patch to the latest FFmpeg master source, and update to use
fmtconvert instead of a naive for loop.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-24 22:18:25 -07:00
Christopher Snowhill
d2eb4af3d5 [Audio Output] Stop immediately, and fix deadlocks
Stop output when requested, except on natural completion of the last
track in the play queue. Also fix deadlocks with stopping and
restarting.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-24 19:15:20 -07:00
Christopher Snowhill
50b7390181 [Vorbis / Opus] Do not assume text encoding
Stream metadata encoding may not be UTF-8, even though the Vorbis
Comment specification clearly calls for this.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-24 19:15:15 -07:00
Christopher Snowhill
abf80c19ac Move static and dynamic libaries to archive
Please remember to unpack the archive before building, and
if it is updated by a future version.
2022-06-24 17:04:57 -07:00
Christopher Snowhill
b21a02fe1b [Sandbox] Change preference dialog descriptions
Make the descriptions more apt to what they do.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-24 17:04:52 -07:00
Christopher Snowhill
dd35639174 [OpenMPT / OpenMPT Legacy] Fix include paths
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-24 17:04:48 -07:00
Christopher Snowhill
43433c244e [mpg123] Fix include paths 2022-06-24 17:04:44 -07:00
Christopher Snowhill
870a5afed7 [Sandbox Broker] Fix deadlock and crash
The crash was because we weren't copying the results array before
iterating over it, and the deadlock was because this was forced to go
through the main thread, rather than going through its calling thread,
which could lock up if the main thread was busy working with the Sandbox
Broker object.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-24 17:04:39 -07:00
Christopher Snowhill
b9ef5853d6 [Metadata] Commit first seen date for whole batch
Commit only once the entire batch is loaded and processed. Also commit
using the correct function.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-24 17:04:35 -07:00
Christopher Snowhill
1a9c73d166 [OpenMPT / vgmstream] Made libraries pre-built
Made the OpenMPT / legacy OpenMPT and mpg123 libraries pre-built.
Changed the OpenMPT and vgmstream plugins to import the libraries as
they are now. Made mpg123 embedded and imported by the main binary,
since it's now shared by two plugins.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-24 17:04:30 -07:00
Christopher Snowhill
ec393d186a [Sandbox Broker] Copy results array
Hopefully this heads off a crash elsewhere.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-24 17:04:16 -07:00
Christopher Snowhill
dfb773e9cb [Audio Output] Properly handle end of playlist
Handle audio on the end of the playlist, flushing playback until all
output stops.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-24 03:46:01 -07:00
Christopher Snowhill
438b142558 [Audio Output] Synchronize access, report latency
Report resampler latency properly, and synchronize access to the
resampler objects.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-24 03:45:16 -07:00
Christopher Snowhill
26a63e85b7 [Visualization] Resample all visualizer audio
Visualizer audio is now resampled to 44100 Hz, for consistency across
the system.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-24 03:43:50 -07:00
Christopher Snowhill
ccbeaf16dc [Audio Output] Resample unsupported sample rates
These rates are too high for Apple's output routines, for some reason.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-24 02:46:23 -07:00
Christopher Snowhill
cc5de69e9f [Core Data Store] Fix startup playlist pruning
The playlist was being pruned of entries marked for deletion, but they
were not being pruned from the set that was then added to the player.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-24 00:34:30 -07:00
Christopher Snowhill
80adb85b36 [Path Suggester] Automatically pop where required
The Path Suggester will now automatically open when new files are added
to the playlist and a given path is not in the sandbox settings. It will
also pop for both the File Tree and MIDI SoundFont path configuration
settings being changed.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-24 00:29:50 -07:00
Christopher Snowhill
f3f3d436ba [Sandbox Broker] Synchronize storage access
Synchronize storage access to main thread only, to prevent enumeration
from hitting a case of the main thread writing to the storage.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-23 23:35:26 -07:00
be6453e048 [Sandbox] Fixed path suggester window
Added a title to the window, make the table view resize properly, and
remove the font size inheritance from the main window.

Signed-off-by: Kevin López Brante <kevin@kddlb.cl>
2022-06-23 23:26:16 -07:00
Christopher Snowhill
8af32e8d2e Replace Core Audio output with Core Media runtime
The output now uses AVSampleBufferAudioRenderer to play all formats, and
uses that to resample. It also supports Spatial Audio on macOS 12.0 or
newer. Note that there are some outstanding bugs with Spatial Audio
support. Namely that it appears to be limited to only 192 kHz at mono or
stereo, or 352800 Hz at surround configurations. This breaks DSD64
playback at stereo formats, as well as possibly other things. This is
entirely an Apple bug. I have reported it to Apple with reference code
FB10441301 for reference, in case anyone else wants to complain that it
isn't fixed.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-23 23:22:41 -07:00
Christopher Snowhill
5b6dacd29c Cog now requires macOS 10.13 as a minimum version
All optional fallback code for older versions has also been removed, and
everything now assumes 10.13.0 or newer. Some cases are still included
for point releases, such as 10.13.2.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-22 22:54:32 -07:00
Christopher Snowhill
ff44bc4d34 Updated VGMStream to r1745-37-g776c4d8c
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-22 19:33:32 -07:00
Christopher Snowhill
62824a94bd Serialize persistent store update to main thread
This needs to be called on the main thread, as something may or may not
be enumerating over the data while this thread decides to call it.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-22 19:11:32 -07:00
Christopher Snowhill
903bc9cba5 [Playlist Info Loader] Do not clear if loading
Do not clear the progress indicator if a loading task is already running
in the background, but instead return without doing anything.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-22 19:05:27 -07:00
Christopher Snowhill
f274a8ef73 Sync version number with main branch 2022-06-22 16:28:08 -07:00
0317f2a649 [About Window] Fix
Pull request #281 by @nevack.
2022-06-22 16:11:59 -07:00
b484a0be44 [About Window] Reorganized credits and added @nevack and myself in them
Signed-off-by: Kevin López Brante <kevin@kddlb.cl>
2022-06-22 16:11:38 -07:00
Dzmitry Neviadomski
36a9411b14 Fix runtime warnings in Window/AboutWindowController.xib
Fix typo in File Owner class name and remove absent outlet.
2022-06-22 16:11:24 -07:00
64fefce18d Merge pull request #280 from losnoco/nevack/about-window
AboutWindow adjustments
2022-06-22 16:07:16 -07:00
Dzmitry Neviadomski
3a6e41cabd AboutWindow adjustments
Allow opening links in default browser
Close window on Esc
Add rounded corners
2022-06-22 16:06:26 -07:00
Christopher Snowhill
632ba36f13 Changed updater script to handle new version strings
New version strings are in a different place, and Sparkle will no longer
be including the Git hash in the CFBundleVersion query, so we must get
it from the ZIP filename.
2022-06-22 01:18:43 -07:00
Christopher Snowhill
271b9b34d0 One last attempt to fix CI
This should fix building. I don't know how I missed those.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-21 23:51:05 -07:00
Christopher Snowhill
8d031f394b Assign blank development team in project files
Hopefully this blank assignment will spare these files from being
touched by Xcode again in the future, when the variable in question is
imported from a developer supplied configuration file.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-21 23:27:53 -07:00
Christopher Snowhill
f2c6ae39c3 Remove developer supplied configuration file
This file should not be referenced directly by projects, otherwise it
will be expected to exist, even in CI.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-21 23:26:51 -07:00
Christopher Snowhill
bb95270747 [Volume Control] Only initialize view once
Only initialize viewController once, the first time the volume control
is opened. Re-initializing it can cause an error assigning it as first
responder to the volume slider popover view.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-21 22:52:05 -07:00
Christopher Snowhill
aa36e3ce10 Completely overhaul code signing practices
Redesign the code signing from the ground up. Now all bundles and their
embedded frameworks import the Shared.xcconfig file and enable its
settings, so they may be signed with Apple Development instead of sign
to run locally. This apparently isn't necessary for frameworks which are
embedded in the main app bundle directly, only for the bundles and their
frameworks.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-21 22:42:33 -07:00
Christopher Snowhill
da8a4dffdf Remove deep forced code signing option
This option should no longer be needed for anything.
2022-06-21 19:40:15 -07:00
a4692b80a4 [Main Menu] Added Privacy Policy link in App Menu
Signed-off-by: Kevin López Brante <kevin@kddlb.cl>
2022-06-21 19:31:06 -07:00
de5cce8351 [About Dialog] Switched to WebView for credits
Signed-off-by: Kevin López Brante <kevin@kddlb.cl>
2022-06-21 19:30:58 -07:00
Christopher Snowhill
05da7450da [Crashlytics] Require asking user consent
Require asking user consent for data transmission on first launch, or
otherwise disable sending crash reports by default.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-21 19:16:23 -07:00
Christopher Snowhill
2b8156e86c [Info Plist] Auto format XML escapes
Automatically format any XML escapes of file type association names.
Adjust Info.plist to account for this change.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-21 19:14:00 -07:00
Christopher Snowhill
d59b5335e9 Revert "Removed Sparkle"
This reverts commit b54ee58ec3.
2022-06-21 18:00:30 -07:00
Christopher Snowhill
bc9e7b5d67 Revert "Remove stray entitlement from Sparkle"
This reverts commit 5ea6c9dde7.
2022-06-21 18:00:09 -07:00
3100 changed files with 195942 additions and 379170 deletions

View file

@ -3,7 +3,7 @@ name: Feedback
about: Report bugs or suggest new features
title: ''
labels:
assignees: kode54, nevack
assignees: kode54
---

View file

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 2809.9 600" style="enable-background:new 0 0 2809.9 600;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FF6336;}
.st1{fill:#FFC501;}
.st2{fill:#A4A14A;}
</style>
<g>
<polygon class="st0" points="393.5,468.8 524.7,468.8 524.7,376.1 355.1,376.1 262.3,468.8 169.6,376.1 0,376.1 0,468.8
131.2,468.8 262.3,600 "/>
<rect y="190.6" class="st1" width="524.7" height="92.8"/>
<rect y="5.1" class="st2" width="524.7" height="92.8"/>
</g>
<path d="M733.3,5.1h83.4v455.2h-83.4V5.1z"/>
<path d="M946.7,447.3c-26.3-14.5-47.2-34.5-62.6-59.7c-15.4-25.3-23.1-53.1-23.1-83.4s7.7-58.2,23.1-83.4
c15.4-25.3,36.2-45.2,62.6-59.7c26.3-14.5,54.9-21.8,85.7-21.8c30.8,0,59.3,7.3,85.7,21.8c26.3,14.5,47.2,34.5,62.6,59.7
c15.4,25.3,23.1,53.1,23.1,83.4s-7.7,58.2-23.1,83.4c-15.4,25.3-36.3,45.2-62.6,59.7c-26.3,14.5-54.9,21.8-85.7,21.8
C1001.5,469.1,973,461.8,946.7,447.3z M1076.2,380.9c13.3-7.4,23.9-17.8,31.9-31.3s12-28.7,12-45.5s-4-32-12-45.5
s-18.6-23.9-31.9-31.3c-13.3-7.4-27.9-11.1-43.9-11.1s-30.7,3.7-43.9,11.1c-13.3,7.4-23.9,17.8-31.9,31.3s-12,28.7-12,45.5
c0,16.9,4,32,12,45.5s18.6,23.9,31.9,31.3c13.3,7.4,27.9,11.1,43.9,11.1C1048.3,391.9,1063,388.3,1076.2,380.9z"/>
<path d="M1247.9,5.1h83.4v271.2L1440,147.9h99.2l-122.6,144.8l131.5,167.5h-106.8l-110-144.8v144.8h-83.4L1247.9,5.1L1247.9,5.1z"/>
<path d="M1626.9,448.8c-23.4-13.5-42.4-32.8-56.9-57.8c-14.5-25.1-21.8-54-21.8-86.9c0-29.9,7-57.5,20.9-82.8s32.9-45.3,56.9-60.1
c24-14.7,50.3-22.1,79-22.1c20.2,0,38.7,3.4,55.3,10.1c16.6,6.7,29.4,15.8,38.3,27.2V148h83.4v312.3h-83.4v-28.4
c-13.1,12.2-27,21.5-41.7,27.8c-14.8,6.3-33.7,9.5-56.9,9.5C1674.6,469.1,1650.2,462.3,1626.9,448.8z M1778.9,366.7
c15.6-16.9,23.4-37.7,23.4-62.6s-7.8-45.7-23.4-62.6c-15.6-16.8-36.2-25.3-61.9-25.3c-25.7,0-46.4,8.4-62,25.3s-23.4,37.7-23.4,62.6
s7.8,45.7,23.4,62.6s36.2,25.3,62,25.3C1742.6,391.9,1763.3,383.5,1778.9,366.7z"/>
<path d="M1942.6,5.1h83.4v455.2h-83.4V5.1z"/>
<path d="M2091.2,89.8C2081,79.7,2076,67.4,2076,53.1c0-14.7,5.1-27.3,15.2-37.6C2101.3,5.2,2113.5,0,2127.8,0
c14.7,0,27.3,5.2,37.6,15.5s15.5,22.9,15.5,37.6c0,14.3-5.2,26.5-15.5,36.7c-10.3,10.1-22.9,15.2-37.6,15.2
C2113.5,104.9,2101.3,99.9,2091.2,89.8z M2086.7,147.9h83.4v312.3h-83.4V147.9z"/>
<path d="M2227.1,438.7l19-78.4h3.8c27.4,21.1,55.4,31.6,84.1,31.6c11.8,0,21.4-2.2,28.8-6.6c7.4-4.4,11.1-10.8,11.1-19.3
c0-8.8-4.3-16-13-21.5c-8.6-5.5-24.8-12.2-48.4-20.2c-24-8-42.7-19.6-55.9-34.8c-13.3-15.2-19.9-33.1-19.9-53.7
c0-29.1,10.8-52.5,32.6-70.2c21.7-17.7,49.2-26.6,82.5-26.6c16.9,0,31.8,1.6,44.9,4.7c13.1,3.2,25.5,8.3,37.3,15.5l3.2,79.6h-4.4
c-15.2-9.7-28.7-17-40.5-21.8s-25.1-7.3-39.8-7.3c-10.5,0-19.2,2.1-25.9,6.3s-10.1,9.7-10.1,16.4c0,8.9,4.2,16.1,12.7,21.8
c8.4,5.7,24.2,12.5,47.4,20.5c26.5,8.9,46.7,19.6,60.4,32.2s20.5,32.2,20.5,58.8c0,21.9-5.5,40.7-16.4,56.3
c-11,15.6-25.4,27.3-43.3,35.1c-17.9,7.8-37.6,11.7-59.1,11.7C2294.9,469.1,2257.8,459,2227.1,438.7z"/>
<path d="M2574.8,446.9c-26.1-14.7-46.7-34.9-61.6-60.4c-15-25.5-22.4-53.8-22.4-85c0-30.3,7.1-57.8,21.2-82.5
c14.1-24.7,33.7-44.1,58.8-58.5c25.1-14.3,53.4-21.5,85-21.5c32,0,59.7,7.5,83.1,22.4c23.4,15,41.1,34.9,53.1,59.7
c12,24.9,18,51.6,18,80.3v24.7h-239.6c5.1,23.6,15.9,41.5,32.6,53.7c16.6,12.2,38.7,18.3,66.1,18.3c41.7,0,78.2-13.7,109.4-41.1h8.9
l-3.2,79c-19,11-39,19.2-60.1,24.7s-41.3,8.2-60.7,8.2C2630.4,469.1,2600.9,461.7,2574.8,446.9z M2726.5,269.3
c-2.1-19-10-33.8-23.7-44.6c-13.7-10.7-30.5-16.1-50.3-16.1c-19.4,0-36.4,5.2-50.9,15.5s-24.3,25.4-29.4,45.2H2726.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

View file

@ -11,14 +11,21 @@ on:
jobs:
build:
name: Build Universal Cog.app
runs-on: macos-11
runs-on: macos-15
env:
XCODE_DERIVEDDATA_PATH: build
steps:
- name: Switch to Xcode 16
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: 16
- name: Check out repository
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
submodules: recursive
- name: Unpack libraries
run: >
cd ThirdParty && tar xvf libraries.tar.xz
- name: Run xcodebuild
run: >
xcodebuild
@ -43,7 +50,7 @@ jobs:
$XCODE_DERIVEDDATA_PATH/Build/Products/Debug/Cog.app
$XCODE_DERIVEDDATA_PATH/Cog.zip
- name: Upload Artifact
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: Cog
path: ${{ env.XCODE_DERIVEDDATA_PATH }}/Cog.zip

49
.gitignore vendored
View file

@ -1,11 +1,56 @@
.DS_Store
xcuserdata
./build
/build
# Special cog exceptions
!Frameworks/OpenMPT/OpenMPT/build
# User-specific xcconfig files
Xcode-config/DEVELOPMENT_TEAM.xcconfig
Xcode-config/SENTRY_SETTINGS.xcconfig
Cog.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
# Plist derived from template at build time
/Info.plist
# This indicates the libraries are up to date
/ThirdParty/libraries.updated
# Temporary file to indicate libraries are being extracted by one process
/ThirdParty/libraries.extracting
# The project will unpack these before building, if necessary
/ThirdParty/BASS/libbass.dylib
/ThirdParty/BASS/libbass_mpc.dylib
/ThirdParty/BASS/libbassflac.dylib
/ThirdParty/BASS/libbassmidi.dylib
/ThirdParty/BASS/libbassopus.dylib
/ThirdParty/BASS/libbasswv.dylib
/ThirdParty/avif/lib/libaom.a
/ThirdParty/avif/lib/libavif.a
/ThirdParty/fdk-aac/lib/libfdk-aac.2.dylib
/ThirdParty/fdk-aac/lib/libfdk-aac.a
/ThirdParty/fdk-aac/lib/libfdk-aac.dylib
/ThirdParty/fdk-aac/lib/libfdk-aac.la
/ThirdParty/fdk-aac/lib/pkgconfig/fdk-aac.pc
/ThirdParty/ffmpeg/lib/libavcodec.61.dylib
/ThirdParty/ffmpeg/lib/libavformat.61.dylib
/ThirdParty/ffmpeg/lib/libavutil.59.dylib
/ThirdParty/ffmpeg/lib/libswresample.5.dylib
/ThirdParty/flac/lib/libFLAC.12.dylib
/ThirdParty/libid3tag/lib/libid3tag.a
/ThirdParty/libmad/lib/libmad.a
/ThirdParty/libopenmpt/lib/libopenmpt.a
/ThirdParty/libopenmpt_old/lib/libopenmpt.old.a
/ThirdParty/libvgm/lib/libvgm-emu.a
/ThirdParty/libvgm/lib/libvgm-player.a
/ThirdParty/libvgm/lib/libvgm-utils.a
/ThirdParty/mpg123/lib/libmpg123.0.dylib
/ThirdParty/ogg/lib/libogg.0.dylib
/ThirdParty/opus/lib/libopus.0.dylib
/ThirdParty/opusfile/lib/libopusfile.0.dylib
/ThirdParty/rubberband/lib/librubberband.3.dylib
/ThirdParty/speex/libspeex.a
/ThirdParty/vorbis/lib/libvorbisfile.3.dylib
/ThirdParty/vorbis/lib/libvorbis.0.dylib
/ThirdParty/soxr/lib/libsoxr.0.dylib
/ThirdParty/WavPack/lib/libwavpack.a

7
.gitmodules vendored
View file

@ -3,7 +3,7 @@
url = https://github.com/kode54/mgba.git
[submodule "Frameworks/AdPlug/AdPlug/adplug"]
path = Frameworks/AdPlug/AdPlug/adplug
url = https://github.com/adplug/adplug.git
url = https://github.com/kode54/adplug.git
[submodule "Frameworks/libbinio/libbinio/libbinio"]
path = Frameworks/libbinio/libbinio/libbinio
url = https://github.com/adplug/libbinio.git
@ -15,10 +15,7 @@
url = https://github.com/Thealexbarney/LibAtrac9.git
[submodule "Frameworks/shpakovski/MASShortcut"]
path = Frameworks/shpakovski/MASShortcut
url = https://github.com/shpakovski/MASShortcut.git
url = https://github.com/kode54/MASShortcut.git
[submodule "Frameworks/libsidplayfp/sidplayfp"]
path = Frameworks/libsidplayfp/sidplayfp
url = https://github.com/kode54/libsidplayfp.git
[submodule "Audio/ThirdParty/r8brain-free-src"]
path = Audio/ThirdParty/r8brain-free-src
url = https://github.com/kode54/r8brain-free-src

Binary file not shown.

View file

@ -7,6 +7,7 @@
@class PlaylistController;
@class PlaylistView;
@class PlaylistLoader;
@class PreferencesController;
@interface AppController : NSObject {
IBOutlet NSObjectController *currentEntryController;
@ -36,6 +37,7 @@
IBOutlet NSMenuItem *showArtistColumn;
IBOutlet NSMenuItem *showAlbumColumn;
IBOutlet NSMenuItem *showGenreColumn;
IBOutlet NSMenuItem *showPlayCountColumn;
IBOutlet NSMenuItem *showLengthColumn;
IBOutlet NSMenuItem *showTrackColumn;
IBOutlet NSMenuItem *showYearColumn;
@ -47,6 +49,8 @@
IBOutlet FileTreeViewController *fileTreeViewController;
IBOutlet PreferencesController *preferencesController;
NSOperationQueue *queue; // Since we are the app delegate, we take care of the op queue
NSMutableSet *expandedNodes;
@ -68,6 +72,8 @@
- (IBAction)openKofiPage:(id)sender;
- (IBAction)openPatreonPage:(id)sender;
- (IBAction)privacyPolicy:(id)sender;
- (IBAction)feedback:(id)sender;
- (void)initDefaults;
@ -97,6 +103,18 @@
- (IBAction)toggleMiniMode:(id)sender;
- (IBAction)toggleToolbarStyle:(id)sender;
- (BOOL)pathSuggesterEmpty;
+ (BOOL)globalPathSuggesterEmpty;
- (void)showPathSuggester;
+ (void)globalShowPathSuggester;
- (void)selectTrack:(id)sender;
- (IBAction)showRubberbandSettings:(id)sender;
+ (void)globalShowRubberbandSettings;
- (IBAction)checkForUpdates:(id)sender;
@property NSWindow *mainWindow;
@property NSWindow *miniWindow;

View file

@ -11,6 +11,7 @@
#import "PlaylistEntry.h"
#import "PlaylistLoader.h"
#import "PlaylistView.h"
#import "RubberbandEngineTransformer.h"
#import "SQLiteStore.h"
#import "SandboxBroker.h"
#import "SpotlightWindowController.h"
@ -27,13 +28,38 @@
#import "Shortcuts.h"
#import <MASShortcut/Shortcut.h>
#import <MASShortcut/MASDictionaryTransformer.h>
@import Firebase;
#import <Sparkle/Sparkle.h>
#import "PreferencesController.h"
#import "FeedbackController.h"
@import Sentry;
void *kAppControllerContext = &kAppControllerContext;
BOOL kAppControllerShuttingDown = NO;
static AppController *kAppController = nil;
@interface SparkleBridge : NSObject
+ (SPUStandardUpdaterController *)sharedStandardUpdaterController;
@end
@implementation SparkleBridge
+ (SPUStandardUpdaterController *)sharedStandardUpdaterController {
static SPUStandardUpdaterController *sharedStandardUpdaterController_ = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedStandardUpdaterController_ = [[SPUStandardUpdaterController alloc] initWithUpdaterDelegate: nil userDriverDelegate: nil];
});
return sharedStandardUpdaterController_;
}
@end
@implementation AppController {
BOOL _isFullToolbarStyle;
}
@ -63,6 +89,18 @@ BOOL kAppControllerShuttingDown = NO;
NSValueTransformer *totalTimeTransformer = [[TotalTimeTransformer alloc] init];
[NSValueTransformer setValueTransformer:totalTimeTransformer
forName:@"TotalTimeTransformer"];
NSValueTransformer *numberHertzToStringTransformer = [[NumberHertzToStringTransformer alloc] init];
[NSValueTransformer setValueTransformer:numberHertzToStringTransformer
forName:@"NumberHertzToStringTransformer"];
NSValueTransformer *rubberbandEngineEnabledTransformer = [[RubberbandEngineEnabledTransformer alloc] init];
[NSValueTransformer setValueTransformer:rubberbandEngineEnabledTransformer
forName:@"RubberbandEngineEnabledTransformer"];
NSValueTransformer *rubberbandEngineHiddenTransformer = [[RubberbandEngineHiddenTransformer alloc] init];
[NSValueTransformer setValueTransformer:rubberbandEngineHiddenTransformer
forName:@"RubberbandEngineHiddenTransformer"];
}
- (id)init {
self = [super init];
@ -70,6 +108,8 @@ BOOL kAppControllerShuttingDown = NO;
[self initDefaults];
queue = [[NSOperationQueue alloc] init];
kAppController = self;
}
return self;
@ -88,8 +128,10 @@ BOOL kAppControllerShuttingDown = NO;
[p beginSheetModalForWindow:mainWindow
completionHandler:^(NSInteger result) {
if(result == NSModalResponseOK) {
[self->playlistLoader willInsertURLs:[p URLs] origin:URLOriginExternal];
[self->playlistLoader didInsertURLs:[self->playlistLoader addURLs:[p URLs] sort:YES] origin:URLOriginExternal];
NSDictionary *loadEntryData = @{@"entries": [p URLs],
@"sort": @(YES),
@"origin": @(URLOriginExternal)};
[self->playlistController performSelectorInBackground:@selector(addURLsInBackground:) withObject:loadEntryData];
} else {
[p close];
}
@ -126,8 +168,10 @@ BOOL kAppControllerShuttingDown = NO;
- (void)openURLPanelDidEnd:(OpenURLPanel *)panel returnCode:(int)returnCode contextInfo:(void *)contextInfo {
if(returnCode == NSModalResponseOK) {
[playlistLoader willInsertURLs:@[[panel url]] origin:URLOriginExternal];
[playlistLoader didInsertURLs:[playlistLoader addURLs:@[[panel url]] sort:NO] origin:URLOriginExternal];
NSDictionary *loadEntriesData = @{ @"entries": @[[panel url]],
@"sort": @(NO),
@"origin": @(URLOriginExternal) };
[playlistController performSelectorInBackground:@selector(addURLsInBackground:) withObject:loadEntriesData];
}
}
@ -143,11 +187,19 @@ BOOL kAppControllerShuttingDown = NO;
return [key isEqualToString:@"currentEntry"];
}
- (void)awakeFromNib {
[[NSUserDefaults standardUserDefaults] registerDefaults:@{ @"NSApplicationCrashOnExceptions": @(YES) }];
static BOOL consentLastEnabled = NO;
[FIRApp configure];
[FIRAnalytics setAnalyticsCollectionEnabled:YES];
- (void)awakeFromNib {
[[NSUserDefaults standardUserDefaults] registerDefaults:@{ @"sentryConsented": @(NO),
@"sentryAskedConsent": @(NO) }];
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.sentryConsented" options:(NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew) context:kAppControllerContext];
#ifdef DEBUG
// Prevent updates automatically in debug builds
[[[SparkleBridge sharedStandardUpdaterController] updater] setAutomaticallyChecksForUpdates:NO];
#endif
[[[SparkleBridge sharedStandardUpdaterController] updater] setUpdateCheckInterval:3600];
[[totalTimeField cell] setBackgroundStyle:NSBackgroundStyleRaised];
@ -158,6 +210,10 @@ BOOL kAppControllerShuttingDown = NO;
[randomizeButton setToolTip:NSLocalizedString(@"RandomizeButtonTooltip", @"")];
[fileButton setToolTip:NSLocalizedString(@"FileButtonTooltip", @"")];
[self registerDefaultHotKeys];
[self migrateHotKeys];
[self registerHotKeys];
(void)[spotlightWindowController init];
@ -187,6 +243,7 @@ BOOL kAppControllerShuttingDown = NO;
if(!sandboxBroker) {
ALog(@"Sandbox broker init failed.");
}
[SandboxBroker cleanupFolderAccess];
[[playlistController undoManager] enableUndoRegistration];
@ -205,7 +262,7 @@ BOOL kAppControllerShuttingDown = NO;
NSError *error = nil;
NSArray *results = [playlistController.persistentContainer.viewContext executeFetchRequest:request error:&error];
if(results && [results count] == 1) {
if(results && [results count] > 0) {
PlaylistEntry *pe = results[0];
if([[NSUserDefaults standardUserDefaults] boolForKey:@"resumePlaybackOnStartup"]) {
[playbackController playEntryAtIndex:pe.index startPaused:(lastStatus == CogStatusPaused) andSeekTo:@(pe.currentPosition)];
@ -216,6 +273,13 @@ BOOL kAppControllerShuttingDown = NO;
pe.countAdded = NO;
[playlistController commitPersistentStore];
}
// Bug fix
if([results count] > 1) {
for(size_t i = 1; i < [results count]; ++i) {
PlaylistEntry *pe = results[i];
[pe setCurrent:NO];
}
}
}
}
@ -239,8 +303,8 @@ BOOL kAppControllerShuttingDown = NO;
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(nodeExpanded:) name:NSOutlineViewItemDidExpandNotification object:outlineView];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(nodeCollapsed:) name:NSOutlineViewItemDidCollapseNotification object:outlineView];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateDockMenu:) name:CogPlaybackDidBeginNotficiation object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateDockMenu:) name:CogPlaybackDidStopNotficiation object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateDockMenu:) name:CogPlaybackDidBeginNotificiation object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateDockMenu:) name:CogPlaybackDidStopNotificiation object:nil];
[self updateDockMenu:nil];
@ -284,8 +348,47 @@ BOOL kAppControllerShuttingDown = NO;
if(context != kAppControllerContext) {
return;
}
if([keyPath isEqualToString:@"values.sentryConsented"]) {
BOOL enabled = [[NSUserDefaults standardUserDefaults] boolForKey:@"sentryConsented"];
if(enabled != consentLastEnabled) {
if(enabled) {
[SentrySDK startWithConfigureOptions:^(SentryOptions *options) {
options.dsn = @"https://b5eda1c2390eb965a74dd735413b6392@cog-analytics.losno.co/3";
options.debug = YES; // Enabled debug when first installing is always helpful
if([keyPath isEqualToString:@"playlistController.currentEntry"]) {
// Temporary until there's a better solution
options.enableAppHangTracking = NO;
// Set tracesSampleRate to 1.0 to capture 100% of transactions for performance monitoring.
// We recommend adjusting this value in production.
options.tracesSampleRate = @1.0;
options.profilesSampleRate = @1.0;
// Adds IP for users.
// For more information, visit: https://docs.sentry.io/platforms/apple/data-management/data-collected/
options.sendDefaultPii = YES;
// And now to set up user feedback prompting
options.onCrashedLastRun = ^void(SentryEvent * _Nonnull event) {
// capture user feedback
FeedbackController *fbcon = [[FeedbackController alloc] init];
[fbcon performSelectorOnMainThread:@selector(showWindow:) withObject:nil waitUntilDone:YES];
if([fbcon waitForCompletion]) {
SentryFeedback *feedback = [[SentryFeedback alloc] initWithMessage:[fbcon comments] name:[fbcon name] email:[fbcon email] source:SentryFeedbackSourceCustom associatedEventId:event.eventId attachments:nil];
[SentrySDK captureFeedback:feedback];
}
};
}];
} else {
if([SentrySDK isEnabled]) {
[SentrySDK close];
}
}
consentLastEnabled = enabled;
}
} else if([keyPath isEqualToString:@"playlistController.currentEntry"]) {
PlaylistEntry *entry = playlistController.currentEntry;
NSString *appTitle = NSLocalizedString(@"CogTitle", @"");
if(!entry) {
@ -460,8 +563,10 @@ BOOL kAppControllerShuttingDown = NO;
- (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filename {
NSArray *urls = @[[NSURL fileURLWithPath:filename]];
[playlistLoader willInsertURLs:urls origin:URLOriginExternal];
[playlistLoader didInsertURLs:[playlistLoader addURLs:urls sort:NO] origin:URLOriginExternal];
NSDictionary *loadEntriesData = @{ @"entries": urls,
@"sort": @(NO),
@"origin": @(URLOriginExternal) };
[playlistController performSelectorInBackground:@selector(addURLsInBackground:) withObject:loadEntriesData];
return YES;
}
@ -470,10 +575,45 @@ BOOL kAppControllerShuttingDown = NO;
NSMutableArray *urls = [NSMutableArray array];
for(NSString *filename in filenames) {
[urls addObject:[NSURL fileURLWithPath:filename]];
NSURL *url = nil;
if([[NSFileManager defaultManager] fileExistsAtPath:filename]) {
url = [NSURL fileURLWithPath:filename];
} else {
if([filename hasPrefix:@"/http/::"] ||
[filename hasPrefix:@"/https/::"]) {
// Stupid Carbon bodge for AppleScript
NSString *method = nil;
NSString *server = nil;
NSString *path = nil;
NSScanner *objScanner = [NSScanner scannerWithString:filename];
if(![objScanner scanString:@"/" intoString:nil] ||
![objScanner scanUpToString:@"/" intoString:&method] ||
![objScanner scanString:@"/::" intoString:nil] ||
![objScanner scanUpToString:@":" intoString:&server] ||
![objScanner scanString:@":" intoString:nil]) {
continue;
}
[objScanner scanUpToCharactersFromSet:[NSCharacterSet illegalCharacterSet] intoString:&path];
// Colons in server were converted to shashes, convert back
NSString *convertedServer = [server stringByReplacingOccurrencesOfString:@"/" withString:@":"];
// Slashes in path were converted to colons, convert back
NSString *convertedPath = [path stringByReplacingOccurrencesOfString:@":" withString:@"/"];
url = [NSURL URLWithString:[NSString stringWithFormat:@"%@://%@/%@", method, convertedServer, convertedPath]];
}
}
if(url) {
[urls addObject:url];
}
}
[playlistLoader willInsertURLs:urls origin:URLOriginExternal];
[playlistLoader didInsertURLs:[playlistLoader addURLs:urls sort:YES] origin:URLOriginExternal];
NSDictionary *loadEntriesData = @{ @"entries": urls,
@"sort": @(YES),
@"origin": @(URLOriginExternal) };
[playlistController performSelectorInBackground:@selector(addURLsInBackground:) withObject:loadEntriesData];
[theApplication replyToOpenOrPrint:NSApplicationDelegateReplySuccess];
}
@ -493,6 +633,10 @@ BOOL kAppControllerShuttingDown = NO;
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"https://www.patreon.com/kode54"]];
}
- (IBAction)privacyPolicy:(id)sender {
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:NSLocalizedString(@"PrivacyPolicyURL", @"Privacy policy URL from Iubenda.")]];
}
- (IBAction)feedback:(id)sender {
NSString *version = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"];
@ -516,14 +660,11 @@ BOOL kAppControllerShuttingDown = NO;
NSMutableDictionary *userDefaultsValuesDict = [NSMutableDictionary dictionary];
// Font defaults
float fFontSize = [NSFont systemFontSizeForControlSize:NSControlSizeSmall];
float fFontSize = [NSFont systemFontSizeForControlSize:NSControlSizeRegular];
NSNumber *fontSize = @(fFontSize);
[userDefaultsValuesDict setObject:fontSize forKey:@"fontSize"];
NSString *feedURLdefault = @"https://cogcdn.cog.losno.co/mercury.xml";
[userDefaultsValuesDict setObject:feedURLdefault forKey:@"SUFeedURL"];
[userDefaultsValuesDict setObject:@"clearAndPlay" forKey:@"openingFilesBehavior"];
[userDefaultsValuesDict setObject:@"enqueueAndPlay" forKey:@"openingFilesBehavior"];
[userDefaultsValuesDict setObject:@"enqueue" forKey:@"openingFilesAlteredBehavior"];
[userDefaultsValuesDict setObject:@"albumGainWithPeak" forKey:@"volumeScaling"];
@ -532,7 +673,7 @@ BOOL kAppControllerShuttingDown = NO;
[userDefaultsValuesDict setObject:@(CogStatusStopped) forKey:@"lastPlaybackStatus"];
[userDefaultsValuesDict setObject:@"dls appl" forKey:@"midiPlugin"];
[userDefaultsValuesDict setObject:@"BASSMIDI" forKey:@"midiPlugin"];
[userDefaultsValuesDict setObject:@"default" forKey:@"midi.flavor"];
@ -548,25 +689,22 @@ BOOL kAppControllerShuttingDown = NO;
NSData *barColor = [colorToValueTransformer reverseTransformedValue:[NSColor colorWithSRGBRed:1.0 green:0.5 blue:0 alpha:1.0]];
NSData *dotColor = [colorToValueTransformer reverseTransformedValue:[NSColor systemRedColor]];
[userDefaultsValuesDict setObject:@(YES) forKey:@"spectrumSceneKit"];
[userDefaultsValuesDict setObject:barColor forKey:@"spectrumBarColor"];
[userDefaultsValuesDict setObject:dotColor forKey:@"spectrumDotColor"];
[userDefaultsValuesDict setObject:@(150.0) forKey:@"synthDefaultSeconds"];
[userDefaultsValuesDict setObject:@(8.0) forKey:@"synthDefaultFadeSeconds"];
[userDefaultsValuesDict setObject:@(2) forKey:@"synthDefaultLoopCount"];
[userDefaultsValuesDict setObject:@(44100) forKey:@"synthSampleRate"];
[userDefaultsValuesDict setObject:@NO forKey:@"alwaysStopAfterCurrent"];
[userDefaultsValuesDict setObject:@YES forKey:@"selectionFollowsPlayback"];
// Register and sync defaults
[[NSUserDefaults standardUserDefaults] registerDefaults:userDefaultsValuesDict];
[[NSUserDefaults standardUserDefaults] synchronize];
// And if the existing feed URL is broken due to my ineptitude with the above defaults, fix it
NSSet<NSString *> *brokenFeedURLs = [NSSet setWithObjects:
@"https://kode54.net/cog/stable.xml",
@"https://kode54.net/cog/mercury.xml"
@"https://www.kode54.net/cog/mercury.xml",
@"https://f.losno.co/cog/mercury.xml",
nil];
NSString *feedURL = [[NSUserDefaults standardUserDefaults] stringForKey:@"SUFeedURL"];
if([brokenFeedURLs containsObject:feedURL]) {
[[NSUserDefaults standardUserDefaults] setValue:feedURLdefault forKey:@"SUFeedURL"];
}
NSString *oldMidiPlugin = [[NSUserDefaults standardUserDefaults] stringForKey:@"midi.plugin"];
if(oldMidiPlugin) {
[[NSUserDefaults standardUserDefaults] setValue:oldMidiPlugin forKey:@"midiPlugin"];
@ -579,9 +717,100 @@ BOOL kAppControllerShuttingDown = NO;
if([[[NSUserDefaults standardUserDefaults] stringForKey:@"midiPlugin"] isEqualToString:@"FluidSynth"]) {
[[NSUserDefaults standardUserDefaults] setValue:@"BASSMIDI" forKey:@"midiPlugin"];
}
NSString *midiPlugin = [[NSUserDefaults standardUserDefaults] stringForKey:@"midiPlugin"];
if([midiPlugin length] == 8 && [[midiPlugin substringFromIndex:4] isEqualToString:@"appl"]) {
[[NSUserDefaults standardUserDefaults] setObject:@"BASSMIDI" forKey:@"midiPlugin"];
}
}
MASShortcut *shortcutWithMigration(NSString *oldKeyCodePrefName,
NSString *oldKeyModifierPrefName,
NSString *newShortcutPrefName,
NSInteger newDefaultKeyCode) {
NSEventModifierFlags defaultModifiers = NSEventModifierFlagControl | NSEventModifierFlagCommand;
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
if([defaults objectForKey:oldKeyCodePrefName]) {
NSInteger oldKeyCode = [defaults integerForKey:oldKeyCodePrefName];
NSEventModifierFlags oldKeyModifiers = [defaults integerForKey:oldKeyModifierPrefName];
// Should we consider temporarily save these values for further migration?
[defaults removeObjectForKey:oldKeyCodePrefName];
[defaults removeObjectForKey:oldKeyModifierPrefName];
return [MASShortcut shortcutWithKeyCode:oldKeyCode modifierFlags:oldKeyModifiers];
} else {
return [MASShortcut shortcutWithKeyCode:newDefaultKeyCode modifierFlags:defaultModifiers];
}
}
static NSDictionary *shortcutDefaults = nil;
- (void)registerDefaultHotKeys {
MASShortcut *playShortcut = shortcutWithMigration(@"hotKeyPlayKeyCode",
@"hotKeyPlayModifiers",
CogPlayShortcutKey,
kVK_ANSI_P);
MASShortcut *nextShortcut = shortcutWithMigration(@"hotKeyNextKeyCode",
@"hotKeyNextModifiers",
CogNextShortcutKey,
kVK_ANSI_N);
MASShortcut *prevShortcut = shortcutWithMigration(@"hotKeyPreviousKeyCode",
@"hotKeyPreviousModifiers",
CogPrevShortcutKey,
kVK_ANSI_R);
MASShortcut *spamShortcut = [MASShortcut shortcutWithKeyCode:kVK_ANSI_C
modifierFlags:NSEventModifierFlagControl | NSEventModifierFlagCommand];
MASShortcut *fadeShortcut = [MASShortcut shortcutWithKeyCode:kVK_ANSI_O
modifierFlags:NSEventModifierFlagControl | NSEventModifierFlagCommand];
MASShortcut *seekBkwdShortcut = [MASShortcut shortcutWithKeyCode:kVK_LeftArrow
modifierFlags:NSEventModifierFlagControl | NSEventModifierFlagCommand];
MASShortcut *seekFwdShortcut = [MASShortcut shortcutWithKeyCode:kVK_RightArrow
modifierFlags:NSEventModifierFlagControl | NSEventModifierFlagCommand];
MASDictionaryTransformer *transformer = [MASDictionaryTransformer new];
NSDictionary *playShortcutDict = [transformer reverseTransformedValue:playShortcut];
NSDictionary *nextShortcutDict = [transformer reverseTransformedValue:nextShortcut];
NSDictionary *prevShortcutDict = [transformer reverseTransformedValue:prevShortcut];
NSDictionary *spamShortcutDict = [transformer reverseTransformedValue:spamShortcut];
NSDictionary *fadeShortcutDict = [transformer reverseTransformedValue:fadeShortcut];
NSDictionary *seekBkwdShortcutDict = [transformer reverseTransformedValue:seekBkwdShortcut];
NSDictionary *seekFwdShortcutDict = [transformer reverseTransformedValue:seekFwdShortcut];
// Register default values to be used for the first app start
NSDictionary<NSString *, NSDictionary *> *defaultShortcuts = @{
CogPlayShortcutKey: playShortcutDict,
CogNextShortcutKey: nextShortcutDict,
CogPrevShortcutKey: prevShortcutDict,
CogSpamShortcutKey: spamShortcutDict,
CogFadeShortcutKey: fadeShortcutDict,
CogSeekBackwardShortcutKey: seekBkwdShortcutDict,
CogSeekForwardShortcutKey: seekFwdShortcutDict
};
shortcutDefaults = defaultShortcuts;
[[NSUserDefaults standardUserDefaults] registerDefaults:defaultShortcuts];
}
- (IBAction)resetHotkeys:(id)sender {
[shortcutDefaults enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
[[NSUserDefaults standardUserDefaults] setObject:obj forKey:key];
}];
}
- (void)migrateHotKeys {
NSArray *inKeys = @[CogPlayShortcutKeyV1, CogNextShortcutKeyV1, CogPrevShortcutKeyV1, CogSpamShortcutKeyV1, CogFadeShortcutKeyV1, CogSeekBackwardShortcutKeyV1, CogSeekForwardShortcutKeyV1];
NSArray *outKeys = @[CogPlayShortcutKey, CogNextShortcutKey, CogPrevShortcutKey, CogSpamShortcutKey, CogFadeShortcutKey, CogSeekBackwardShortcutKey, CogSeekForwardShortcutKey];
for(size_t i = 0, j = [inKeys count]; i < j; ++i) {
NSString *inKey = inKeys[i];
NSString *outKey = outKeys[i];
id value = [[NSUserDefaults standardUserDefaults] objectForKey:inKey];
if(value && value != [NSNull null]) {
[[NSUserDefaults standardUserDefaults] setObject:value forKey:outKey];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:inKey];
}
}
}
/* Unassign previous handler first, so dealloc can unregister it from the global map before the new instances are assigned */
- (void)registerHotKeys {
MASShortcutBinder *binder = [MASShortcutBinder sharedBinder];
[binder bindShortcutWithDefaultsKey:CogPlayShortcutKey
@ -603,6 +832,21 @@ BOOL kAppControllerShuttingDown = NO;
toAction:^{
[self clickSpam];
}];
[binder bindShortcutWithDefaultsKey:CogFadeShortcutKey
toAction:^{
[self clickFade];
}];
[binder bindShortcutWithDefaultsKey:CogSeekBackwardShortcutKey
toAction:^{
[self clickSeekBack];
}];
[binder bindShortcutWithDefaultsKey:CogSeekForwardShortcutKey
toAction:^{
[self clickSeekForward];
}];
}
- (void)clickPlay {
@ -629,10 +873,22 @@ BOOL kAppControllerShuttingDown = NO;
[playbackController spam:nil];
}
- (void)clickFade {
[playbackController fade:nil];
}
- (void)clickSeek:(NSTimeInterval)position {
[playbackController seek:self toTime:position];
}
- (void)clickSeekBack {
[playbackController seekBackward:10.0];
}
- (void)clickSeekForward {
[playbackController seekForward:10.0];
}
- (void)changeFontSize:(float)size {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
float fCurrentSize = [defaults floatForKey:@"fontSize"];
@ -702,7 +958,7 @@ BOOL kAppControllerShuttingDown = NO;
BOOL hideItem = NO;
if([[notification name] isEqualToString:CogPlaybackDidStopNotficiation] || !pe || ![pe artist] || [[pe artist] isEqualToString:@""])
if([[notification name] isEqualToString:CogPlaybackDidStopNotificiation] || !pe || ![pe artist] || [[pe artist] isEqualToString:@""])
hideItem = YES;
if(hideItem && [dockMenu indexOfItem:currentArtistItem] == 0) {
@ -712,4 +968,41 @@ BOOL kAppControllerShuttingDown = NO;
}
}
- (BOOL)pathSuggesterEmpty {
return [playlistController pathSuggesterEmpty];
}
+ (BOOL)globalPathSuggesterEmpty {
return [kAppController pathSuggesterEmpty];
}
- (void)showPathSuggester {
[preferencesController showPathSuggester:self];
}
+ (void)globalShowPathSuggester {
[kAppController showPathSuggester];
}
- (void)showRubberbandSettings:(id)sender {
[preferencesController showRubberbandSettings:sender];
}
+ (void)globalShowRubberbandSettings {
[kAppController showRubberbandSettings:kAppController];
}
- (IBAction)checkForUpdates:(id)sender {
[[SparkleBridge sharedStandardUpdaterController] checkForUpdates:[[NSApplication sharedApplication] delegate]];
}
- (void)selectTrack:(id)sender {
PlaylistEntry *pe = (PlaylistEntry *)sender;
@try {
[playlistView selectRowIndexes:[NSIndexSet indexSetWithIndex:pe.index] byExtendingSelection:NO];
}
@catch(NSException *e) {
}
}
@end

View file

@ -13,6 +13,13 @@
@interface DockIconController : NSObject {
NSImage *dockImage;
NSInteger lastDockCustom;
NSInteger lastDockCustomPlaque;
NSInteger dockCustomLoaded;
NSImage *dockCustomStop;
NSImage *dockCustomPlay;
NSImage *dockCustomPause;
IBOutlet PlaybackController *playbackController;
NSInteger lastPlaybackStatus;

View file

@ -14,16 +14,24 @@
static NSString *DockIconPlaybackStatusObservationContext = @"DockIconPlaybackStatusObservationContext";
static NSString *CogCustomDockIconsReloadNotification = @"CogCustomDockIconsReloadNotification";
- (void)startObserving {
[playbackController addObserver:self forKeyPath:@"playbackStatus" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial) context:(__bridge void *_Nullable)(DockIconPlaybackStatusObservationContext)];
[playbackController addObserver:self forKeyPath:@"progressOverall" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionOld) context:(__bridge void *_Nullable)(DockIconPlaybackStatusObservationContext)];
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.colorfulDockIcons" options:0 context:(__bridge void *_Nullable)(DockIconPlaybackStatusObservationContext)];
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.customDockIcons" options:0 context:(__bridge void *_Nullable)(DockIconPlaybackStatusObservationContext)];
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.customDockIconsPlaque" options:0 context:(__bridge void *_Nullable)(DockIconPlaybackStatusObservationContext)];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(refreshDockIcons:) name:CogCustomDockIconsReloadNotification object:nil];
}
- (void)stopObserving {
[playbackController removeObserver:self forKeyPath:@"playbackStatus" context:(__bridge void *_Nullable)(DockIconPlaybackStatusObservationContext)];
[playbackController removeObserver:self forKeyPath:@"progressOverall" context:(__bridge void *_Nullable)(DockIconPlaybackStatusObservationContext)];
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.colorfulDockIcons" context:(__bridge void *_Nullable)(DockIconPlaybackStatusObservationContext)];
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.customDockIcons" context:(__bridge void *_Nullable)(DockIconPlaybackStatusObservationContext)];
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.customDockIconsPlaque" context:(__bridge void *_Nullable)(DockIconPlaybackStatusObservationContext)];
[[NSNotificationCenter defaultCenter] removeObserver:self name:CogCustomDockIconsReloadNotification object:nil];
}
- (void)startObservingProgress:(NSProgress *)progress {
@ -42,6 +50,34 @@ static NSString *getBadgeName(NSString *baseName, BOOL colorfulIcons) {
}
}
static NSString *getCustomIconName(NSString *baseName) {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES);
NSString *basePath = [[paths firstObject] stringByAppendingPathComponent:@"Cog"];
basePath = [basePath stringByAppendingPathComponent:@"Icons"];
basePath = [basePath stringByAppendingPathComponent:baseName];
return [basePath stringByAppendingPathExtension:@"png"];
}
- (BOOL)loadCustomDockIcons {
NSError *error = nil;
NSData *dataStopIcon = [NSData dataWithContentsOfFile:getCustomIconName(@"Stop") options:(NSDataReadingMappedIfSafe) error:&error];
if(!dataStopIcon || error) {
return NO;
}
NSData *dataPlayIcon = [NSData dataWithContentsOfFile:getCustomIconName(@"Play") options:(NSDataReadingMappedIfSafe) error:&error];
if(!dataPlayIcon || error) {
return NO;
}
NSData *dataPauseIcon = [NSData dataWithContentsOfFile:getCustomIconName(@"Pause") options:(NSDataReadingMappedIfSafe) error:&error];
if(!dataPauseIcon || error) {
return NO;
}
dockCustomStop = [[NSImage alloc] initWithData:dataStopIcon];
dockCustomPlay = [[NSImage alloc] initWithData:dataPlayIcon];
dockCustomPause = [[NSImage alloc] initWithData:dataPauseIcon];
return (dockCustomStop && dockCustomPlay && dockCustomPause);
}
- (void)refreshDockIcon:(NSInteger)playbackStatus withProgress:(double)progressStatus {
// Really weird crash user experienced because the plaque image didn't load?
if(!dockImage || dockImage.size.width == 0 || dockImage.size.height == 0) return;
@ -50,6 +86,30 @@ static NSString *getBadgeName(NSString *baseName, BOOL colorfulIcons) {
BOOL drawIcon = NO;
BOOL removeProgress = NO;
BOOL useCustomDockIcons = [[NSUserDefaults standardUserDefaults] boolForKey:@"customDockIcons"];
BOOL useCustomDockIconsPlaque = [[NSUserDefaults standardUserDefaults] boolForKey:@"customDockIconsPlaque"];
if(useCustomDockIcons && !dockCustomLoaded) {
dockCustomLoaded = [self loadCustomDockIcons];
if(!dockCustomLoaded) {
useCustomDockIcons = NO;
}
}
if(useCustomDockIcons != lastDockCustom ||
useCustomDockIconsPlaque != lastDockCustomPlaque) {
lastDockCustom = useCustomDockIcons;
lastDockCustomPlaque = useCustomDockIconsPlaque;
drawIcon = YES;
if(!useCustomDockIcons) {
dockCustomLoaded = NO;
dockCustomStop = nil;
dockCustomPlay = nil;
dockCustomPause = nil;
}
}
if(playbackStatus < 0)
playbackStatus = lastPlaybackStatus;
else {
@ -82,20 +142,20 @@ static NSString *getBadgeName(NSString *baseName, BOOL colorfulIcons) {
if(drawIcon) {
switch(playbackStatus) {
case CogStatusPlaying:
badgeImage = [NSImage imageNamed:getBadgeName(@"Play", colorfulIcons)];
badgeImage = useCustomDockIcons ? dockCustomPlay : [NSImage imageNamed:getBadgeName(@"Play", colorfulIcons)];
break;
case CogStatusPaused:
badgeImage = [NSImage imageNamed:getBadgeName(@"Pause", colorfulIcons)];
badgeImage = useCustomDockIcons ? dockCustomPause : [NSImage imageNamed:getBadgeName(@"Pause", colorfulIcons)];
break;
default:
badgeImage = [NSImage imageNamed:getBadgeName(@"Stop", colorfulIcons)];
badgeImage = useCustomDockIcons ? dockCustomStop : [NSImage imageNamed:getBadgeName(@"Stop", colorfulIcons)];
break;
}
NSSize badgeSize = [badgeImage size];
NSImage *newDockImage = [dockImage copy];
NSImage *newDockImage = (useCustomDockIcons && !useCustomDockIconsPlaque) ? [[NSImage alloc] initWithSize:NSMakeSize(1024, 1024)] : [dockImage copy];
[newDockImage lockFocus];
[badgeImage drawInRect:NSMakeRect(0, 0, 1024, 1024)
@ -110,7 +170,7 @@ static NSString *getBadgeName(NSString *baseName, BOOL colorfulIcons) {
[dockTile setContentView:imageView];
progressIndicator = [[NSProgressIndicator alloc] initWithFrame:NSMakeRect(0.0, 0.0, dockTile.size.width, 10.0)];
[progressIndicator setStyle:NSProgressIndicatorBarStyle];
[progressIndicator setStyle:NSProgressIndicatorStyleBar];
[progressIndicator setIndeterminate:NO];
[progressIndicator setBezeled:YES];
[progressIndicator setMinValue:0];
@ -186,7 +246,9 @@ static NSString *getBadgeName(NSString *baseName, BOOL colorfulIcons) {
}
[self refreshDockIcon:-1 withProgress:progressStatus];
} else if([keyPath isEqualToString:@"values.colorfulDockIcons"]) {
} else if([keyPath isEqualToString:@"values.colorfulDockIcons"] ||
[keyPath isEqualToString:@"values.customDockIcons"] ||
[keyPath isEqualToString:@"values.customDockIconsPlaque"]) {
[self refreshDockIcon:-1 withProgress:-10];
} else if([keyPath isEqualToString:@"fractionCompleted"]) {
double progressStatus = [(NSProgress *)object fractionCompleted];
@ -197,6 +259,12 @@ static NSString *getBadgeName(NSString *baseName, BOOL colorfulIcons) {
}
}
- (void)refreshDockIcons:(NSNotification *)notification {
lastDockCustom = NO;
dockCustomLoaded = NO;
[self refreshDockIcon:-1 withProgress:-10];
}
- (void)awakeFromNib {
dockImage = [[NSImage imageNamed:@"Plaque"] copy];
lastColorfulStatus = -1;

View file

@ -19,10 +19,16 @@
#define DEFAULT_VOLUME_DOWN 5
#define DEFAULT_VOLUME_UP DEFAULT_VOLUME_DOWN
extern NSString *CogPlaybackDidBeginNotficiation;
extern NSString *CogPlaybackDidPauseNotficiation;
extern NSString *CogPlaybackDidResumeNotficiation;
extern NSString *CogPlaybackDidStopNotficiation;
#define DEFAULT_PITCH_DOWN 0.2
#define DEFAULT_PITCH_UP DEFAULT_PITCH_DOWN
#define DEFAULT_TEMPO_DOWN 0.2
#define DEFAULT_TEMPO_UP DEFAULT_TEMPO_DOWN
extern NSString *CogPlaybackDidBeginNotificiation;
extern NSString *CogPlaybackDidPauseNotificiation;
extern NSString *CogPlaybackDidResumeNotificiation;
extern NSString *CogPlaybackDidStopNotificiation;
extern NSDictionary *makeRGInfo(PlaylistEntry *pe);
@ -40,6 +46,9 @@ extern NSDictionary *makeRGInfo(PlaylistEntry *pe);
IBOutlet EqualizerWindowController *equalizerWindowController;
IBOutlet NSSlider *volumeSlider;
IBOutlet NSSlider *pitchSlider;
IBOutlet NSSlider *tempoSlider;
IBOutlet NSButton *lockButton;
IBOutlet NSArrayController *outputDevices;
@ -69,6 +78,14 @@ extern NSDictionary *makeRGInfo(PlaylistEntry *pe);
- (IBAction)volumeDown:(id)sender;
- (IBAction)volumeUp:(id)sender;
- (IBAction)changePitch:(id)sender;
- (IBAction)pitchDown:(id)sender;
- (IBAction)pitchUp:(id)sender;
- (IBAction)changeTempo:(id)sender;
- (IBAction)tempoDown:(id)sender;
- (IBAction)tempoUp:(id)sender;
- (IBAction)playPauseResume:(id)sender;
- (IBAction)pauseResume:(id)sender;
- (IBAction)skipToNextAlbum:(id)sender;

View file

@ -19,18 +19,76 @@
#import "Logging.h"
@import Firebase;
@import Sentry;
// Sentry captureMessage is too spammy to use for anything but actual errors
extern BOOL kAppControllerShuttingDown;
@implementation NSObject (NxAdditions)
#if 0
-(void)performSelectorInBackground:(SEL)selector withObjects:(id)object, ...
{
NSMethodSignature *signature = [self methodSignatureForSelector:selector];
// Setup the invocation
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
invocation.target = self;
invocation.selector = selector;
// Associate the arguments
va_list objects;
va_start(objects, object);
unsigned int objectCounter = 2;
for (id obj = object; obj != nil; obj = va_arg(objects, id))
{
[invocation setArgument:&obj atIndex:objectCounter++];
}
va_end(objects);
// Make sure to invoke on a background queue
NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithInvocation:invocation];
NSOperationQueue *backgroundQueue = [[NSOperationQueue alloc] init];
[backgroundQueue addOperation:operation];
}
#endif
-(void)performSelectorOnMainThread:(SEL)selector withObjects:(id)object, ...
{
NSMethodSignature *signature = [self methodSignatureForSelector:selector];
// Setup the invocation
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
invocation.target = self;
invocation.selector = selector;
// Associate the arguments
va_list objects;
va_start(objects, object);
unsigned int objectCounter = 2;
for (id obj = object; obj != nil; obj = va_arg(objects, id))
{
[invocation setArgument:&obj atIndex:objectCounter++];
}
va_end(objects);
// Invoke on the main operation queue
NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithInvocation:invocation];
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
[mainQueue addOperation:operation];
}
@end
@implementation PlaybackController
#define DEFAULT_SEEK 5
NSString *CogPlaybackDidBeginNotficiation = @"CogPlaybackDidBeginNotficiation";
NSString *CogPlaybackDidPauseNotficiation = @"CogPlaybackDidPauseNotficiation";
NSString *CogPlaybackDidResumeNotficiation = @"CogPlaybackDidResumeNotficiation";
NSString *CogPlaybackDidStopNotficiation = @"CogPlaybackDidStopNotficiation";
NSString *CogPlaybackDidBeginNotificiation = @"CogPlaybackDidBeginNotificiation";
NSString *CogPlaybackDidPauseNotificiation = @"CogPlaybackDidPauseNotificiation";
NSString *CogPlaybackDidResumeNotificiation = @"CogPlaybackDidResumeNotificiation";
NSString *CogPlaybackDidStopNotificiation = @"CogPlaybackDidStopNotificiation";
@synthesize playbackStatus;
@ -62,15 +120,51 @@ NSString *CogPlaybackDidStopNotficiation = @"CogPlaybackDidStopNotficiation";
- (void)initDefaults {
NSDictionary *defaultsDictionary = @{ @"volume": @(75.0),
@"pitch": @(1.0),
@"tempo": @(1.0),
@"speedLock": @(YES),
@"GraphicEQenable": @(NO),
@"GraphicEQpreset": @(-1),
@"GraphicEQtrackgenre": @(NO),
@"volumeLimit": @(YES),
@"headphoneVirtualization": @(NO) };
@"enableHrtf": @(NO),
@"enableHeadTracking": @(NO),
@"enableHDCD": @(NO),
/*@"rubberbandEngine": @"faster",*/
@"rubberbandTransients": @"crisp",
@"rubberbandDetector": @"compound",
@"rubberbandPhase": @"laminar",
@"rubberbandWindow": @"standard",
@"rubberbandSmoothing": @"off",
@"rubberbandFormant": @"shifted",
@"rubberbandPitch": @"highspeed",
@"rubberbandChannels": @"apart"
};
[[NSUserDefaults standardUserDefaults] registerDefaults:defaultsDictionary];
}
static double speedScale(double input, double min, double max) {
input = (input - min) * 100.0 / (max - min);
return ((input * input) * (5.0 - 0.2) / 10000.0) + 0.2;
}
static double reverseSpeedScale(double input, double min, double max) {
input = sqrtf((input - 0.2) * 10000.0 / (5.0 - 0.2));
return (input * (max - min) / 100.0) + min;
}
- (void)snapSpeeds {
double pitch = [[NSUserDefaults standardUserDefaults] doubleForKey:@"pitch"];
double tempo = [[NSUserDefaults standardUserDefaults] doubleForKey:@"tempo"];
if(fabs(pitch - 1.0) < 1e-6) {
[[NSUserDefaults standardUserDefaults] setDouble:1.0 forKey:@"pitch"];
}
if(fabs(tempo - 1.0) < 1e-6) {
[[NSUserDefaults standardUserDefaults] setDouble:1.0 forKey:@"tempo"];
}
}
- (void)awakeFromNib {
BOOL volumeLimit = [[[NSUserDefaultsController sharedUserDefaultsController] defaults] boolForKey:@"volumeLimit"];
const double MAX_VOLUME = (volumeLimit) ? 100.0 : 800.0;
@ -80,6 +174,16 @@ NSString *CogPlaybackDidStopNotficiation = @"CogPlaybackDidStopNotficiation";
[volumeSlider setDoubleValue:logarithmicToLinear(volume, MAX_VOLUME)];
[audioPlayer setVolume:volume];
double pitch = [[NSUserDefaults standardUserDefaults] doubleForKey:@"pitch"];
[pitchSlider setDoubleValue:reverseSpeedScale(pitch, [pitchSlider minValue], [pitchSlider maxValue])];
double tempo = [[NSUserDefaults standardUserDefaults] doubleForKey:@"tempo"];
[tempoSlider setDoubleValue:reverseSpeedScale(tempo, [tempoSlider minValue], [tempoSlider maxValue])];
[self snapSpeeds];
BOOL speedLock = [[NSUserDefaults standardUserDefaults] boolForKey:@"speedLock"];
[lockButton setTitle:speedLock ? @"🔒" : @"🔓"];
[self setSeekable:NO];
}
@ -101,6 +205,11 @@ NSString *CogPlaybackDidStopNotficiation = @"CogPlaybackDidStopNotficiation";
}
- (IBAction)pause:(id)sender {
if(![self seekable]) {
[self stop:sender];
return;
}
[[NSUserDefaults standardUserDefaults] setInteger:CogStatusPaused forKey:@"lastPlaybackStatus"];
[audioPlayer pause];
@ -192,11 +301,11 @@ NSDictionary *makeRGInfo(PlaylistEntry *pe) {
if(!pe.url) {
pe.error = YES;
pe.errorMessage = NSLocalizedStringFromTableInBundle(@"ErrorMessageBadFile", nil, [NSBundle bundleForClass:[self class]], @"");
[[FIRCrashlytics crashlytics] log:@"Attempting to play bad file."];
[SentrySDK captureMessage:@"Attempted to play a bad file with no URL"];
return;
}
[[FIRCrashlytics crashlytics] logWithFormat:@"Playing track: %@", pe.url];
//[SentrySDK captureMessage:[NSString stringWithFormat:@"Playing track: %@", pe.url]];
DLog(@"PLAYLIST CONTROLLER: %@", [playlistController class]);
[playlistController setCurrentEntry:pe];
@ -216,7 +325,7 @@ NSDictionary *makeRGInfo(PlaylistEntry *pe) {
#if 0
// Race here, but the worst that could happen is we re-read the data
if ([pe metadataLoaded] != YES) {
if([pe metadataLoaded] != YES) {
[pe performSelectorOnMainThread:@selector(setMetadata:) withObject:[playlistLoader readEntryInfo:pe] waitUntilDone:YES];
}
#elif 0
@ -237,7 +346,7 @@ NSDictionary *makeRGInfo(PlaylistEntry *pe) {
double seekTime = pe.seekable ? [offset doubleValue] : 0.0;
[audioPlayer play:pe.url withUserInfo:pe withRGInfo:makeRGInfo(pe) startPaused:paused andSeekTo:seekTime];
[audioPlayer performSelectorOnMainThread:@selector(playBG:withUserInfo:withRGInfo:startPaused:andSeekTo:) withObjects:pe.url, pe, makeRGInfo(pe), @(paused), @(seekTime), nil];
}
- (IBAction)next:(id)sender {
@ -248,8 +357,12 @@ NSDictionary *makeRGInfo(PlaylistEntry *pe) {
}
- (IBAction)prev:(id)sender {
if([playlistController prev] == NO)
return;
double pos = [audioPlayer amountPlayed];
if(pos < 5.0) {
if([playlistController prev] == NO)
return;
}
[self playEntry:[playlistController currentEntry]];
}
@ -268,7 +381,7 @@ NSDictionary *makeRGInfo(PlaylistEntry *pe) {
- (IBAction)seek:(id)sender {
double time = [sender doubleValue];
[audioPlayer seekToTime:time];
[audioPlayer performSelectorOnMainThread:@selector(seekToTimeBG:) withObjects:@(time), nil];
lastPosition = -10;
@ -285,7 +398,7 @@ NSDictionary *makeRGInfo(PlaylistEntry *pe) {
lastPosition = -10;
[audioPlayer seekToTime:time];
[audioPlayer performSelectorOnMainThread:@selector(seekToTimeBG:) withObjects:@(time), nil];
[self setPosition:time];
@ -311,14 +424,16 @@ NSDictionary *makeRGInfo(PlaylistEntry *pe) {
}
- (void)seekForward:(double)amount {
double seekTo = [audioPlayer amountPlayed] + amount;
if(seekTo > [[[playlistController currentEntry] length] doubleValue]) {
[self next:self];
} else {
lastPosition = -10;
[audioPlayer seekToTime:seekTo];
[self setPosition:seekTo];
if([self seekable]) {
double seekTo = [audioPlayer amountPlayed] + amount;
if(seekTo > [[[playlistController currentEntry] length] doubleValue]) {
[self next:self];
} else {
lastPosition = -10;
[audioPlayer performSelectorOnMainThread:@selector(seekToTimeBG:) withObjects:@(seekTo), nil];
[self setPosition:seekTo];
}
}
}
@ -327,15 +442,17 @@ NSDictionary *makeRGInfo(PlaylistEntry *pe) {
}
- (void)seekBackward:(double)amount {
double seekTo = [audioPlayer amountPlayed] - amount;
if(seekTo < 0)
seekTo = 0;
lastPosition = -10;
[audioPlayer seekToTime:seekTo];
[self setPosition:seekTo];
if([self seekable]) {
double seekTo = [audioPlayer amountPlayed] - amount;
if(seekTo < 0)
seekTo = 0;
lastPosition = -10;
[audioPlayer performSelectorOnMainThread:@selector(seekToTimeBG:) withObjects:@(seekTo), nil];
[self setPosition:seekTo];
}
}
/*
@ -344,7 +461,7 @@ NSDictionary *makeRGInfo(PlaylistEntry *pe) {
NSImage *img = [NSImage imageNamed:name];
// [img retain];
if (img == nil)
if(img == nil)
{
DLog(@"Error loading image!");
}
@ -445,6 +562,32 @@ NSDictionary *makeRGInfo(PlaylistEntry *pe) {
}
}
- (IBAction)changePitch:(id)sender {
const double pitch = speedScale([sender doubleValue], [pitchSlider minValue], [pitchSlider maxValue]);
DLog(@"PITCH: %lf", pitch);
[[NSUserDefaults standardUserDefaults] setDouble:pitch forKey:@"pitch"];
if([[NSUserDefaults standardUserDefaults] boolForKey:@"speedLock"]) {
[[NSUserDefaults standardUserDefaults] setDouble:pitch forKey:@"tempo"];
}
[self snapSpeeds];
}
- (IBAction)changeTempo:(id)sender {
const double tempo = speedScale([sender doubleValue], [tempoSlider minValue], [tempoSlider maxValue]);
DLog(@"TEMPO: %lf", tempo);
[[NSUserDefaults standardUserDefaults] setDouble:tempo forKey:@"tempo"];
if([[NSUserDefaults standardUserDefaults] boolForKey:@"speedLock"]) {
[[NSUserDefaults standardUserDefaults] setDouble:tempo forKey:@"pitch"];
}
[self snapSpeeds];
}
- (IBAction)skipToNextAlbum:(id)sender {
BOOL found = NO;
@ -546,8 +689,75 @@ NSDictionary *makeRGInfo(PlaylistEntry *pe) {
[[NSUserDefaults standardUserDefaults] setDouble:[audioPlayer volume] forKey:@"volume"];
}
- (void)audioPlayer:(AudioPlayer *)player displayEqualizer:(AudioUnit)eq {
- (IBAction)pitchDown:(id)sender {
double pitch = speedScale([pitchSlider doubleValue], [pitchSlider minValue], [pitchSlider maxValue]);
double newPitch = pitch - DEFAULT_PITCH_DOWN;
if(newPitch < 0.2) {
newPitch = 0.2;
}
[pitchSlider setDoubleValue:reverseSpeedScale(newPitch, [pitchSlider minValue], [pitchSlider maxValue])];
[[NSUserDefaults standardUserDefaults] setDouble:newPitch forKey:@"pitch"];
if([[NSUserDefaults standardUserDefaults] boolForKey:@"speedLock"]) {
[tempoSlider setDoubleValue:reverseSpeedScale(newPitch, [tempoSlider minValue], [tempoSlider maxValue])];
[[NSUserDefaults standardUserDefaults] setDouble:newPitch forKey:@"tempo"];
}
}
- (IBAction)pitchUp:(id)sender {
double pitch = speedScale([pitchSlider doubleValue], [pitchSlider minValue], [pitchSlider maxValue]);
double newPitch = pitch + DEFAULT_PITCH_UP;
if(newPitch > 5.0) {
newPitch = 5.0;
}
[pitchSlider setDoubleValue:reverseSpeedScale(newPitch, [pitchSlider minValue], [pitchSlider maxValue])];
[[NSUserDefaults standardUserDefaults] setDouble:newPitch forKey:@"pitch"];
if([[NSUserDefaults standardUserDefaults] boolForKey:@"speedLock"]) {
[tempoSlider setDoubleValue:reverseSpeedScale(newPitch, [tempoSlider minValue], [tempoSlider maxValue])];
[[NSUserDefaults standardUserDefaults] setDouble:newPitch forKey:@"tempo"];
}
}
- (IBAction)tempoDown:(id)sender {
double tempo = speedScale([tempoSlider doubleValue], [tempoSlider minValue], [tempoSlider maxValue]);
double newTempo = tempo - DEFAULT_TEMPO_DOWN;
if(newTempo < 0.2) {
newTempo = 0.2;
}
[tempoSlider setDoubleValue:reverseSpeedScale(newTempo, [tempoSlider minValue], [tempoSlider maxValue])];
[[NSUserDefaults standardUserDefaults] setDouble:newTempo forKey:@"tempo"];
if([[NSUserDefaults standardUserDefaults] boolForKey:@"speedLock"]) {
[pitchSlider setDoubleValue:reverseSpeedScale(newTempo, [pitchSlider minValue], [pitchSlider maxValue])];
[[NSUserDefaults standardUserDefaults] setDouble:newTempo forKey:@"pitch"];
}
}
- (IBAction)tempoUp:(id)sender {
double tempo = speedScale([tempoSlider doubleValue], [tempoSlider minValue], [tempoSlider maxValue]);
double newTempo = tempo + DEFAULT_TEMPO_UP;
if(newTempo > 5.0) {
newTempo = 5.0;
}
[tempoSlider setDoubleValue:reverseSpeedScale(newTempo, [tempoSlider minValue], [tempoSlider maxValue])];
[[NSUserDefaults standardUserDefaults] setDouble:newTempo forKey:@"tempo"];
if([[NSUserDefaults standardUserDefaults] boolForKey:@"speedLock"]) {
[pitchSlider setDoubleValue:reverseSpeedScale(newTempo, [pitchSlider minValue], [pitchSlider maxValue])];
[[NSUserDefaults standardUserDefaults] setDouble:newTempo forKey:@"pitch"];
}
}
- (void)audioPlayer:(AudioPlayer *)player displayEqualizer:(AudioUnit)eq {
if(_eq && _eq != eq) {
[equalizerWindowController setEQ:nil];
}
@ -565,19 +775,6 @@ NSDictionary *makeRGInfo(PlaylistEntry *pe) {
- (void)audioPlayer:(AudioPlayer *)player removeEqualizer:(AudioUnit)eq {
if(eq == _eq) {
OSStatus err;
CFPropertyListRef classData;
UInt32 size;
size = sizeof(classData);
err = AudioUnitGetProperty(eq, kAudioUnitProperty_ClassInfo, kAudioUnitScope_Global, 0, &classData, &size);
if(err == noErr) {
CFPreferencesSetAppValue(CFSTR("GraphEQ_Preset"), classData, kCFPreferencesCurrentApplication);
CFRelease(classData);
}
CFPreferencesAppSynchronize(kCFPreferencesCurrentApplication);
[equalizerWindowController setEQ:nil];
_eq = nil;
@ -599,15 +796,15 @@ NSDictionary *makeRGInfo(PlaylistEntry *pe) {
}
if(pe && pe.url) {
[[FIRCrashlytics crashlytics] logWithFormat:@"Beginning decoding track: %@", pe.url];
//[SentrySDK captureMessage:[NSString stringWithFormat:@"Beginning decoding track: %@", pe.url]];
[player setNextStream:pe.url withUserInfo:pe withRGInfo:makeRGInfo(pe)];
} else if(pe) {
[[FIRCrashlytics crashlytics] log:@"Invalid playlist entry reached."];
[SentrySDK captureMessage:@"Invalid playlist entry reached"];
[player setNextStream:nil];
pe.error = YES;
pe.errorMessage = NSLocalizedStringFromTableInBundle(@"ErrorMessageBadFile", nil, [NSBundle bundleForClass:[self class]], @"");
} else {
[[FIRCrashlytics crashlytics] log:@"End of playlist reached."];
//[SentrySDK captureMessage:@"End of playlist reached"];
[player setNextStream:nil];
}
}
@ -617,12 +814,15 @@ NSDictionary *makeRGInfo(PlaylistEntry *pe) {
// Delay the action until this function has returned to the audio thread
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_MSEC), dispatch_get_main_queue(), ^{
[[FIRCrashlytics crashlytics] logWithFormat:@"Updating UI with track: %@", pe.url];
if(pe) {
//[SentrySDK captureMessage:[NSString stringWithFormat:@"Updating UI with track: %@", pe.url]];
}
[self->playlistController setCurrentEntry:pe];
if(self->_eq)
if(pe && self->_eq) {
equalizerApplyGenre(self->_eq, [pe genre]);
}
self->lastPosition = -10;
@ -631,7 +831,9 @@ NSDictionary *makeRGInfo(PlaylistEntry *pe) {
[self removeHDCD:nil];
});
[[NSNotificationCenter defaultCenter] postNotificationName:CogPlaybackDidBeginNotficiation object:pe];
if(pe) {
[[NSNotificationCenter defaultCenter] postNotificationName:CogPlaybackDidBeginNotificiation object:pe];
}
}
- (void)audioPlayer:(AudioPlayer *)player didChangeStatus:(NSNumber *)s userInfo:(id)userInfo {
@ -644,26 +846,26 @@ NSDictionary *makeRGInfo(PlaylistEntry *pe) {
}
if(status == CogStatusStopped) {
[[FIRCrashlytics crashlytics] log:@"Stopped."];
//[SentrySDK captureMessage:@"Playback stopped"];
[self setPosition:0];
[self setSeekable:NO]; // the player stopped, disable the slider
[[NSNotificationCenter defaultCenter] postNotificationName:CogPlaybackDidStopNotficiation object:nil];
[[NSNotificationCenter defaultCenter] postNotificationName:CogPlaybackDidStopNotificiation object:nil];
} else // paused
{
[[FIRCrashlytics crashlytics] log:@"Paused."];
[[NSNotificationCenter defaultCenter] postNotificationName:CogPlaybackDidPauseNotficiation object:nil];
//[SentrySDK captureMessage:@"Playback paused"];
[[NSNotificationCenter defaultCenter] postNotificationName:CogPlaybackDidPauseNotificiation object:nil];
}
} else if(status == CogStatusPlaying) {
[[FIRCrashlytics crashlytics] log:@"Started playing."];
//[SentrySDK captureMessage:@"Playback started"];
if(!positionTimer) {
positionTimer = [NSTimer timerWithTimeInterval:0.2 target:self selector:@selector(updatePosition:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:positionTimer forMode:NSRunLoopCommonModes];
}
[[NSNotificationCenter defaultCenter] postNotificationName:CogPlaybackDidResumeNotficiation object:nil];
[[NSNotificationCenter defaultCenter] postNotificationName:CogPlaybackDidResumeNotificiation object:nil];
}
if(status == CogStatusStopped) {
@ -685,14 +887,6 @@ NSDictionary *makeRGInfo(PlaylistEntry *pe) {
break;
}
if(status == CogStatusStopped) {
status = CogStatusStopping;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
if([self playbackStatus] == CogStatusStopping)
[self setPlaybackStatus:CogStatusStopped];
});
}
[self setPlaybackStatus:status];
// If we don't send it here, if we've stopped, then the NPIC will be stuck at the last file we played.
[self sendMetaData];
@ -700,7 +894,7 @@ NSDictionary *makeRGInfo(PlaylistEntry *pe) {
- (void)audioPlayer:(AudioPlayer *)player didStopNaturally:(id)userInfo {
if([[NSUserDefaults standardUserDefaults] boolForKey:@"quitOnNaturalStop"]) {
[[FIRCrashlytics crashlytics] log:@"Terminating due to natural stop."];
//[SentrySDK captureMessage:@"Playback stopped naturally, terminating app"];
[NSApp terminate:nil];
}
}
@ -715,20 +909,21 @@ NSDictionary *makeRGInfo(PlaylistEntry *pe) {
- (void)audioPlayer:(AudioPlayer *)player restartPlaybackAtCurrentPosition:(id)userInfo {
PlaylistEntry *pe = [playlistController currentEntry];
BOOL paused = playbackStatus == CogStatusPaused;
[[FIRCrashlytics crashlytics] logWithFormat:@"Restarting playback of track: %@", pe.url];
[player play:pe.url withUserInfo:pe withRGInfo:makeRGInfo(pe) startPaused:paused andSeekTo:pe.seekable ? pe.currentPosition : 0.0];
//[SentrySDK captureMessage:[NSString stringWithFormat:@"Playback restarting for track: %@", pe.url]];
[player performSelectorOnMainThread:@selector(playBG:withUserInfo:withRGInfo:startPaused:andSeekTo:) withObjects:pe.url, pe, makeRGInfo(pe), @(paused), @(pe.seekable ? pe.currentPosition : 0.0), nil];
}
- (void)audioPlayer:(AudioPlayer *)player pushInfo:(NSDictionary *)info toTrack:(id)userInfo {
PlaylistEntry *pe = (PlaylistEntry *)userInfo;
if (!pe) pe = [playlistController currentEntry];
if(!pe) pe = [playlistController currentEntry];
[pe setMetadata:info];
[playlistView refreshTrack:pe];
// Delay the action until this function has returned to the audio thread
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_MSEC), dispatch_get_main_queue(), ^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 50 * NSEC_PER_MSEC), dispatch_get_main_queue(), ^{
self->playlistController.currentEntry = pe;
[self sendMetaData];
[[NSNotificationCenter defaultCenter] postNotificationName:CogPlaybackDidBeginNotificiation object:pe];
});
[[NSNotificationCenter defaultCenter] postNotificationName:CogPlaybackDidBeginNotficiation object:pe];
}
- (void)audioPlayer:(AudioPlayer *)player reportPlayCountForTrack:(id)userInfo {
@ -738,6 +933,15 @@ NSDictionary *makeRGInfo(PlaylistEntry *pe) {
}
}
- (void)audioPlayer:(AudioPlayer *)player updatePosition:(id)userInfo {
if(userInfo) {
PlaylistEntry *pe = (PlaylistEntry *)userInfo;
if([pe current]) {
[self updatePosition:userInfo];
}
}
}
- (void)audioPlayer:(AudioPlayer *)player setError:(NSNumber *)status toTrack:(id)userInfo {
PlaylistEntry *pe = (PlaylistEntry *)userInfo;
[pe setError:[status boolValue]];
@ -817,8 +1021,10 @@ NSDictionary *makeRGInfo(PlaylistEntry *pe) {
if(entry.year) {
// If PlaylistEntry can represent a full date like some tag formats can do, change it
NSCalendar *calendar = [NSCalendar currentCalendar];
NSDate *releaseYear = [calendar dateWithEra:1 year:entry.year month:0 day:0 hour:0 minute:0 second:0 nanosecond:0];
[songInfo setObject:releaseYear forKey:MPMediaItemPropertyReleaseDate];
NSDate *releaseYear = [calendar dateWithEra:1 year:entry.year month:1 day:1 hour:0 minute:0 second:0 nanosecond:0];
if(releaseYear) {
[songInfo setObject:releaseYear forKey:MPMediaItemPropertyReleaseDate];
}
}
[songInfo setObject:@(entry.currentPosition) forKey:MPNowPlayingInfoPropertyElapsedPlaybackTime];
[songInfo setObject:entry.length forKey:MPMediaItemPropertyPlaybackDuration];

View file

@ -322,19 +322,19 @@ didReceiveNotificationResponse:(UNNotificationResponse *)response
- (void)awakeFromNib {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(playbackDidBegin:)
name:CogPlaybackDidBeginNotficiation
name:CogPlaybackDidBeginNotificiation
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(playbackDidPause:)
name:CogPlaybackDidPauseNotficiation
name:CogPlaybackDidPauseNotificiation
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(playbackDidResume:)
name:CogPlaybackDidResumeNotficiation
name:CogPlaybackDidResumeNotificiation
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(playbackDidStop:)
name:CogPlaybackDidStopNotficiation
name:CogPlaybackDidStopNotificiation
object:nil];
}

View file

@ -13,5 +13,6 @@
}
+ (NSArray *)urlsForContainerURL:(NSURL *)url;
+ (NSArray *)dependencyUrlsForContainerURL:(NSURL *)url;
@end

View file

@ -18,4 +18,10 @@
}
}
+ (NSArray *)dependencyUrlsForContainerURL:(NSURL *)url {
@autoreleasepool {
return [[PluginController sharedPluginController] dependencyUrlsForContainerURL:url];
}
}
@end

View file

@ -8,7 +8,7 @@
#import <Cocoa/Cocoa.h>
#import "Plugin.h"
#import <CogAudio/Plugin.h>
@interface AudioDecoder : NSObject {
}

View file

@ -8,7 +8,7 @@
#import <Cocoa/Cocoa.h>
#import <CogAudio/Semaphore.h>
#import <CogAudio/CogSemaphore.h>
#import <AVFoundation/AVFoundation.h>
#import <AudioToolbox/AudioToolbox.h>
@ -16,13 +16,7 @@
#import <CoreAudio/CoreAudio.h>
#import <CoreAudio/CoreAudioTypes.h>
#ifdef __cplusplus
#import <atomic>
using std::atomic_int;
using std::atomic_bool;
#else
#import <stdatomic.h>
#endif
@class BufferChain;
@class OutputNode;
@ -32,6 +26,8 @@ using std::atomic_bool;
OutputNode *output;
double volume;
double pitch;
double tempo;
NSMutableArray *chainQueue;
@ -39,6 +35,8 @@ using std::atomic_bool;
id nextStreamUserInfo;
NSDictionary *nextStreamRGInfo;
id previousUserInfo; // Track currently last heard track for play counts
id delegate;
BOOL outputLaunched;
@ -65,12 +63,14 @@ using std::atomic_bool;
- (void)play:(NSURL *)url withUserInfo:(id)userInfo withRGInfo:(NSDictionary *)rgi;
- (void)play:(NSURL *)url withUserInfo:(id)userInfo withRGInfo:(NSDictionary *)rgi startPaused:(BOOL)paused;
- (void)play:(NSURL *)url withUserInfo:(id)userInfo withRGInfo:(NSDictionary *)rgi startPaused:(BOOL)paused andSeekTo:(double)time;
- (void)playBG:(NSURL *)url withUserInfo:(id)userInfo withRGInfo:(NSDictionary *)rgi startPaused:(NSNumber *)paused andSeekTo:(NSNumber *)time;
- (void)stop;
- (void)pause;
- (void)resume;
- (void)seekToTime:(double)time;
- (void)seekToTimeBG:(NSNumber *)time;
- (void)setVolume:(double)v;
- (double)volume;
- (double)volumeUp:(double)amount;
@ -116,6 +116,7 @@ using std::atomic_bool;
- (void)setShouldContinue:(BOOL)s;
//- (BufferChain *)bufferChain;
- (void)launchOutputThread;
- (BOOL)selectNextBuffer;
- (void)endOfInputPlayed;
- (void)reportPlayCount;
- (void)sendDelegateMethod:(SEL)selector withVoid:(void *)obj waitUntilDone:(BOOL)wait;
@ -137,5 +138,6 @@ using std::atomic_bool;
- (void)audioPlayer:(AudioPlayer *)player restartPlaybackAtCurrentPosition:(id)userInfo;
- (void)audioPlayer:(AudioPlayer *)player pushInfo:(NSDictionary *)info toTrack:(id)userInfo;
- (void)audioPlayer:(AudioPlayer *)player reportPlayCountForTrack:(id)userInfo;
- (void)audioPlayer:(AudioPlayer *)player updatePosition:(id)userInfo;
- (void)audioPlayer:(AudioPlayer *)player setError:(NSNumber *)status toTrack:(id)userInfo;
@end

View file

@ -24,6 +24,10 @@
bufferChain = nil;
outputLaunched = NO;
endOfInputReached = NO;
// Safety
pitch = 1.0;
tempo = 1.0;
chainQueue = [[NSMutableArray alloc] init];
@ -56,16 +60,27 @@
[self play:url withUserInfo:userInfo withRGInfo:rgi startPaused:paused andSeekTo:0.0];
}
- (void)playBG:(NSURL *)url withUserInfo:(id)userInfo withRGInfo:(NSDictionary *)rgi startPaused:(NSNumber *)paused andSeekTo:(NSNumber *)time {
@synchronized (self) {
[self play:url withUserInfo:userInfo withRGInfo:rgi startPaused:[paused boolValue] andSeekTo:[time doubleValue]];
}
}
- (void)play:(NSURL *)url withUserInfo:(id)userInfo withRGInfo:(NSDictionary *)rgi startPaused:(BOOL)paused andSeekTo:(double)time {
[self play:url withUserInfo:userInfo withRGInfo:rgi startPaused:paused andSeekTo:time andResumeInterval:NO];
}
- (void)play:(NSURL *)url withUserInfo:(id)userInfo withRGInfo:(NSDictionary *)rgi startPaused:(BOOL)paused andSeekTo:(double)time andResumeInterval:(BOOL)resumeInterval {
ALog(@"Opening file for playback: %@ at seek offset %f%@", url, time, (paused) ? @", starting paused" : @"");
[self waitUntilCallbacksExit];
if(output) {
[output setShouldContinue:NO];
output = nil;
[output fadeOutBackground];
}
if(!output) {
output = [[OutputNode alloc] initWithController:self previous:nil];
[output setupWithInterval:resumeInterval];
}
output = [[OutputNode alloc] initWithController:self previous:nil];
[output setup];
[output setVolume:volume];
@synchronized(chainQueue) {
for(id anObject in chainQueue) {
@ -81,13 +96,19 @@
}
bufferChain = [[BufferChain alloc] initWithController:self];
[self notifyStreamChanged:userInfo];
if(!resumeInterval) {
[self notifyStreamChanged:userInfo];
}
while(![bufferChain open:url withOutputFormat:[output format] withOutputConfig:[output config] withUserInfo:userInfo withRGInfo:rgi]) {
while(![bufferChain open:url withOutputFormat:[output format] withUserInfo:userInfo withRGInfo:rgi]) {
bufferChain = nil;
[self requestNextStream:userInfo];
if([nextStream isEqualTo:url]) {
return;
}
url = nextStream;
if(url == nil) {
return;
@ -108,14 +129,23 @@
[self setShouldContinue:YES];
outputLaunched = NO;
if(!resumeInterval) {
outputLaunched = NO;
}
startedPaused = paused;
initialBufferFilled = NO;
previousUserInfo = userInfo;
[bufferChain launchThreads];
if(paused)
if(paused) {
[self setPlaybackStatus:CogStatusPaused waitUntilDone:YES];
if(time > 0.0) {
[self updatePosition:userInfo];
}
} else if(resumeInterval) {
[output fadeIn];
}
}
- (void)stop {
@ -133,11 +163,15 @@
bufferChain = nil;
}
}
if(output) {
[output setShouldContinue:NO];
[output close];
}
output = nil;
}
- (void)pause {
[output pause];
[output fadeOut];
[self setPlaybackStatus:CogStatusPaused waitUntilDone:YES];
}
@ -149,34 +183,29 @@
[self launchOutputThread];
}
[output fadeIn];
[output resume];
[self setPlaybackStatus:CogStatusPlaying waitUntilDone:YES];
}
- (void)seekToTimeBG:(NSNumber *)time {
[self seekToTime:[time doubleValue]];
}
- (void)seekToTime:(double)time {
if(endOfInputReached) {
// This is a dirty hack in case the playback has finished with the track
// that the user thinks they're seeking into
CogStatus status = (CogStatus)currentPlaybackStatus;
NSURL *url;
id userInfo;
NSDictionary *rgi;
CogStatus status = (CogStatus)currentPlaybackStatus;
NSURL *url;
id userInfo;
NSDictionary *rgi;
@synchronized(chainQueue) {
url = [bufferChain streamURL];
userInfo = [bufferChain userInfo];
rgi = [bufferChain rgInfo];
}
[self stop];
[self play:url withUserInfo:userInfo withRGInfo:rgi startPaused:(status == CogStatusPaused) andSeekTo:time];
} else {
// Still decoding the current file, safe to seek within it
[output seek:time];
[bufferChain seek:time];
@synchronized(chainQueue) {
url = [bufferChain streamURL];
userInfo = [bufferChain userInfo];
rgi = [bufferChain rgInfo];
}
[self play:url withUserInfo:userInfo withRGInfo:rgi startPaused:(status == CogStatusPaused) andSeekTo:time andResumeInterval:YES];
}
- (void)setVolume:(double)v {
@ -219,7 +248,11 @@
}
- (void)restartPlaybackAtCurrentPosition {
[self sendDelegateMethod:@selector(audioPlayer:restartPlaybackAtCurrentPosition:) withObject:[bufferChain userInfo] waitUntilDone:NO];
[self sendDelegateMethod:@selector(audioPlayer:restartPlaybackAtCurrentPosition:) withObject:previousUserInfo waitUntilDone:NO];
}
- (void)updatePosition:(id)userInfo {
[self sendDelegateMethod:@selector(audioPlayer:updatePosition:) withObject:userInfo waitUntilDone:NO];
}
- (void)pushInfo:(NSDictionary *)info toTrack:(id)userInfo {
@ -290,6 +323,8 @@
- (BOOL)endOfInputReached:(BufferChain *)sender // Sender is a BufferChain
{
previousUserInfo = [sender userInfo];
BufferChain *newChain = nil;
if(atomic_load_explicit(&resettingNow, memory_order_relaxed))
@ -302,6 +337,8 @@
// if there's already one at the head of chainQueue... r-r-right?
for(BufferChain *chain in chainQueue) {
if([chain isRunning]) {
if(output)
[output setShouldPlayOutBuffer:YES];
atomic_fetch_sub(&refCount, 1);
return YES;
}
@ -325,6 +362,8 @@
while(duration >= 30.0 && shouldContinue) {
[semaphore wait];
if(atomic_load_explicit(&resettingNow, memory_order_relaxed)) {
if(output)
[output setShouldPlayOutBuffer:YES];
atomic_fetch_sub(&refCount, 1);
return YES;
}
@ -344,83 +383,170 @@
[self requestNextStream:nextStreamUserInfo];
if(!nextStream) {
if(output)
[output setShouldPlayOutBuffer:YES];
atomic_fetch_sub(&refCount, 1);
return YES;
}
BufferChain *lastChain;
@synchronized(chainQueue) {
newChain = [[BufferChain alloc] initWithController:self];
endOfInputReached = YES;
BufferChain *lastChain = [chainQueue lastObject];
lastChain = [chainQueue lastObject];
if(lastChain == nil) {
lastChain = bufferChain;
}
}
BOOL pathsEqual = NO;
BOOL pathsEqual = NO;
if([nextStream isFileURL] && [[lastChain streamURL] isFileURL]) {
NSString *unixPathNext = [nextStream path];
NSString *unixPathPrev = [[lastChain streamURL] path];
if([nextStream isFileURL] && [[lastChain streamURL] isFileURL]) {
NSString *unixPathNext = [nextStream path];
NSString *unixPathPrev = [[lastChain streamURL] path];
if([unixPathNext isEqualToString:unixPathPrev])
pathsEqual = YES;
if([unixPathNext isEqualToString:unixPathPrev])
pathsEqual = YES;
} else if(![nextStream isFileURL] && ![[lastChain streamURL] isFileURL]) {
@try {
NSURL *lastURL = [lastChain streamURL];
NSString *nextScheme = [nextStream scheme];
NSString *lastScheme = [lastURL scheme];
NSString *nextHost = [nextStream host];
NSString *lastHost = [lastURL host];
NSString *nextPath = [nextStream path];
NSString *lastPath = [lastURL path];
if(nextScheme && lastScheme && [nextScheme isEqualToString:lastScheme]) {
if((!nextHost && !lastHost) ||
(nextHost && lastHost && [nextHost isEqualToString:lastHost])) {
if(nextPath && lastPath && [nextPath isEqualToString:lastPath]) {
pathsEqual = YES;
}
}
}
}
@catch(NSException *e) {
DLog(@"Exception thrown checking file match: %@", e);
}
}
if(pathsEqual || ([[nextStream scheme] isEqualToString:[[lastChain streamURL] scheme]] && (([nextStream host] == nil && [[lastChain streamURL] host] == nil) || [[nextStream host] isEqualToString:[[lastChain streamURL] host]]) && [[nextStream path] isEqualToString:[[lastChain streamURL] path]])) {
if([lastChain setTrack:nextStream] && [newChain openWithInput:[lastChain inputNode] withOutputFormat:[output format] withOutputConfig:[output config] withUserInfo:nextStreamUserInfo withRGInfo:nextStreamRGInfo]) {
[newChain setStreamURL:nextStream];
if(pathsEqual) {
if([lastChain setTrack:nextStream] && [newChain openWithInput:[lastChain inputNode] withOutputFormat:[output format] withUserInfo:nextStreamUserInfo withRGInfo:nextStreamRGInfo]) {
[newChain setStreamURL:nextStream];
@synchronized(chainQueue) {
[self addChainToQueue:newChain];
DLog(@"TRACK SET!!! %@", newChain);
// Keep on-playin
newChain = nil;
atomic_fetch_sub(&refCount, 1);
return NO;
}
}
lastChain = nil;
while(shouldContinue && ![newChain open:nextStream withOutputFormat:[output format] withOutputConfig:[output config] withUserInfo:nextStreamUserInfo withRGInfo:nextStreamRGInfo]) {
if(nextStream == nil) {
newChain = nil;
atomic_fetch_sub(&refCount, 1);
return YES;
}
DLog(@"TRACK SET!!! %@", newChain);
// Keep on-playin
newChain = nil;
[self requestNextStream:nextStreamUserInfo];
newChain = [[BufferChain alloc] initWithController:self];
atomic_fetch_sub(&refCount, 1);
return NO;
}
}
[self addChainToQueue:newChain];
lastChain = nil;
NSURL *url = nextStream;
while(shouldContinue && ![newChain open:url withOutputFormat:[output format] withUserInfo:nextStreamUserInfo withRGInfo:nextStreamRGInfo]) {
if(nextStream == nil) {
newChain = nil;
if(output)
[output setShouldPlayOutBuffer:YES];
atomic_fetch_sub(&refCount, 1);
return YES;
}
newChain = nil;
[self requestNextStream:nextStreamUserInfo];
// I'm stupid and can't hold too much stuff in my head all at once, so writing it here.
//
// Once we get here:
// - buffer chain for previous stream finished reading
// - there are (probably) some bytes of the previous stream in the output buffer which haven't been played
// (by output node) yet
// - self.bufferChain == previous playlist entry's buffer chain
// - self.nextStream == next playlist entry's URL
// - self.nextStreamUserInfo == next playlist entry
// - head of chainQueue is the buffer chain for the next entry (which has launched its threads already)
if([nextStream isEqualTo:url]) {
newChain = nil;
if(output)
[output setShouldPlayOutBuffer:YES];
atomic_fetch_sub(&refCount, 1);
return YES;
}
url = nextStream;
newChain = [[BufferChain alloc] initWithController:self];
}
@synchronized(chainQueue) {
[self addChainToQueue:newChain];
}
newChain = nil;
// I'm stupid and can't hold too much stuff in my head all at once, so writing it here.
//
// Once we get here:
// - buffer chain for previous stream finished reading
// - there are (probably) some bytes of the previous stream in the output buffer which haven't been played
// (by output node) yet
// - self.bufferChain == previous playlist entry's buffer chain
// - self.nextStream == next playlist entry's URL
// - self.nextStreamUserInfo == next playlist entry
// - head of chainQueue is the buffer chain for the next entry (which has launched its threads already)
if(output)
[output setShouldPlayOutBuffer:YES];
atomic_fetch_sub(&refCount, 1);
return YES;
}
- (void)reportPlayCount {
if(bufferChain) {
[self reportPlayCountForTrack:[bufferChain userInfo]];
[self reportPlayCountForTrack:previousUserInfo];
}
- (BOOL)selectNextBuffer {
BOOL signalStopped = NO;
do {
@synchronized(chainQueue) {
endOfInputReached = NO;
if([chainQueue count] <= 0) {
// End of playlist
signalStopped = YES;
break;
}
[bufferChain setShouldContinue:NO];
bufferChain = nil;
bufferChain = [chainQueue objectAtIndex:0];
[chainQueue removeObjectAtIndex:0];
DLog(@"New!!! %@ %@", bufferChain, [[bufferChain inputNode] decoder]);
[semaphore signal];
}
} while(0);
if(signalStopped) {
double latency = 0;
if(output) latency = [output latency];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, latency * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[self stop];
self->bufferChain = nil;
[self notifyPlaybackStopped:nil];
});
return YES;
}
[output setEndOfStream:NO];
return NO;
}
- (void)endOfInputPlayed {
@ -428,32 +554,8 @@
// - the buffer chain for the next playlist entry (started in endOfInputReached) have been working for some time
// already, so that there is some decoded and converted data to play
// - the buffer chain for the next entry is the first item in chainQueue
@synchronized(chainQueue) {
endOfInputReached = NO;
if([chainQueue count] <= 0) {
// End of playlist
[self stop];
bufferChain = nil;
[self notifyPlaybackStopped:nil];
return;
}
bufferChain = nil;
bufferChain = [chainQueue objectAtIndex:0];
[chainQueue removeObjectAtIndex:0];
DLog(@"New!!! %@ %@", bufferChain, [[bufferChain inputNode] decoder]);
[semaphore signal];
}
[self notifyStreamChanged:[bufferChain userInfo]];
[output setEndOfStream:NO];
previousUserInfo = [bufferChain userInfo];
[self notifyStreamChanged:previousUserInfo];
}
- (BOOL)chainQueueHasTracks {

View file

@ -65,12 +65,17 @@ enum {
AudioStreamBasicDescription format;
NSMutableData *chunkData;
uint32_t channelConfig;
double streamTimestamp;
double streamTimeRatio;
BOOL formatAssigned;
BOOL lossless;
BOOL hdcd;
}
@property AudioStreamBasicDescription format;
@property uint32_t channelConfig;
@property double streamTimestamp;
@property double streamTimeRatio;
@property BOOL lossless;
+ (uint32_t)guessChannelConfig:(uint32_t)channelCount;
@ -80,16 +85,25 @@ enum {
+ (uint32_t)findChannelIndex:(uint32_t)flag;
- (id)init;
- (id)initWithProperties:(NSDictionary *)properties;
- (void)assignSamples:(const void *)data frameCount:(size_t)count;
- (void)assignSamples:(const void *_Nonnull)data frameCount:(size_t)count;
- (void)assignData:(NSData *)data;
- (NSData *)removeSamples:(size_t)frameCount;
- (BOOL)isEmpty;
- (size_t)frameCount;
- (void)setFrameCount:(size_t)count; // For truncation only
- (double)duration;
- (double)durationRatioed;
- (BOOL)isHDCD;
- (void)setHDCD;
- (AudioChunk *)copy;
@end

View file

@ -7,6 +7,8 @@
#import "AudioChunk.h"
#import "CoreAudioUtils.h"
@implementation AudioChunk
- (id)init {
@ -16,11 +18,40 @@
chunkData = [[NSMutableData alloc] init];
formatAssigned = NO;
lossless = NO;
hdcd = NO;
streamTimestamp = 0.0;
streamTimeRatio = 1.0;
}
return self;
}
- (id)initWithProperties:(NSDictionary *)properties {
self = [super init];
if(self) {
chunkData = [[NSMutableData alloc] init];
[self setFormat:propertiesToASBD(properties)];
lossless = [[properties objectForKey:@"encoding"] isEqualToString:@"lossless"];
hdcd = NO;
streamTimestamp = 0.0;
streamTimeRatio = 1.0;
}
return self;
}
- (AudioChunk *)copy {
AudioChunk *outputChunk = [[AudioChunk alloc] init];
[outputChunk setFormat:format];
[outputChunk setChannelConfig:channelConfig];
if(hdcd) [outputChunk setHDCD];
[outputChunk setStreamTimestamp:streamTimestamp];
[outputChunk setStreamTimeRatio:streamTimeRatio];
[outputChunk assignData:chunkData];
return outputChunk;
}
static const uint32_t AudioChannelConfigTable[] = {
0,
AudioConfigMono,
@ -102,6 +133,8 @@ static const uint32_t AudioChannelConfigTable[] = {
}
@synthesize lossless;
@synthesize streamTimestamp;
@synthesize streamTimeRatio;
- (AudioStreamBasicDescription)format {
return format;
@ -126,20 +159,29 @@ static const uint32_t AudioChannelConfigTable[] = {
channelConfig = config;
}
- (void)assignSamples:(const void *)data frameCount:(size_t)count {
- (void)assignSamples:(const void *_Nonnull)data frameCount:(size_t)count {
if(formatAssigned) {
const size_t bytesPerPacket = format.mBytesPerPacket;
[chunkData appendBytes:data length:bytesPerPacket * count];
}
}
- (void)assignData:(NSData *)data {
[chunkData appendData:data];
}
- (NSData *)removeSamples:(size_t)frameCount {
if(formatAssigned) {
const size_t bytesPerPacket = format.mBytesPerPacket;
const size_t byteCount = bytesPerPacket * frameCount;
NSData *ret = [chunkData subdataWithRange:NSMakeRange(0, byteCount)];
[chunkData replaceBytesInRange:NSMakeRange(0, byteCount) withBytes:NULL length:0];
return ret;
@autoreleasepool {
const double secondsDuration = (double)(frameCount) / format.mSampleRate;
const double DSDrate = (format.mBitsPerChannel == 1) ? 8.0 : 1.0;
const size_t bytesPerPacket = format.mBytesPerPacket;
const size_t byteCount = bytesPerPacket * frameCount;
NSData *ret = [chunkData subdataWithRange:NSMakeRange(0, byteCount)];
[chunkData replaceBytesInRange:NSMakeRange(0, byteCount) withBytes:NULL length:0];
streamTimestamp += secondsDuration * streamTimeRatio * DSDrate;
return ret;
}
}
return [NSData data];
}
@ -156,13 +198,36 @@ static const uint32_t AudioChannelConfigTable[] = {
return 0;
}
- (double)duration {
- (void)setFrameCount:(size_t)count {
if(formatAssigned) {
count *= format.mBytesPerPacket;
size_t currentLength = [chunkData length];
if(count < currentLength) {
[chunkData replaceBytesInRange:NSMakeRange(count, currentLength - count) withBytes:NULL length:0];
}
}
}
- (double)duration {
if(formatAssigned && [chunkData length]) {
const size_t bytesPerPacket = format.mBytesPerPacket;
const double sampleRate = format.mSampleRate;
return (double)([chunkData length] / bytesPerPacket) / sampleRate;
const double DSDrate = (format.mBitsPerChannel == 1) ? 8.0 : 1.0;
return ((double)([chunkData length] / bytesPerPacket) / sampleRate) * DSDrate;
}
return 0.0;
}
- (double)durationRatioed {
return [self duration] * streamTimeRatio;
}
- (BOOL)isHDCD {
return hdcd;
}
- (void)setHDCD {
hdcd = YES;
}
@end

View file

@ -8,9 +8,9 @@
#import <Cocoa/Cocoa.h>
#import "AudioPlayer.h"
#import "ConverterNode.h"
#import "InputNode.h"
#import <CogAudio/AudioPlayer.h>
#import <CogAudio/ConverterNode.h>
#import <CogAudio/InputNode.h>
@interface BufferChain : NSObject {
InputNode *inputNode;
@ -26,17 +26,16 @@
}
- (id)initWithController:(id)c;
- (void)buildChain;
- (BOOL)buildChain;
- (BOOL)open:(NSURL *)url withOutputFormat:(AudioStreamBasicDescription)outputFormat withOutputConfig:(uint32_t)outputConfig withUserInfo:(id)userInfo withRGInfo:(NSDictionary *)rgi;
- (BOOL)open:(NSURL *)url withOutputFormat:(AudioStreamBasicDescription)outputFormat withUserInfo:(id)userInfo withRGInfo:(NSDictionary *)rgi;
// Used when changing tracks to reuse the same decoder
- (BOOL)openWithInput:(InputNode *)i withOutputFormat:(AudioStreamBasicDescription)outputFormat withOutputConfig:(uint32_t)outputConfig withUserInfo:(id)userInfo withRGInfo:(NSDictionary *)rgi;
- (BOOL)openWithInput:(InputNode *)i withOutputFormat:(AudioStreamBasicDescription)outputFormat withUserInfo:(id)userInfo withRGInfo:(NSDictionary *)rgi;
// Used when resetting the decoder on seek
- (BOOL)openWithDecoder:(id<CogDecoder>)decoder
withOutputFormat:(AudioStreamBasicDescription)outputFormat
withOutputConfig:(uint32_t)outputConfig
withUserInfo:(id)userInfo
withRGInfo:(NSDictionary *)rgi;

View file

@ -9,8 +9,11 @@
#import "BufferChain.h"
#import "AudioSource.h"
#import "CoreAudioUtils.h"
#import "DSPDownmixNode.h"
#import "OutputNode.h"
#import "AudioPlayer.h"
#import "Logging.h"
@implementation BufferChain
@ -30,21 +33,32 @@
return self;
}
- (void)buildChain {
inputNode = nil;
- (BOOL)buildChain {
// Cut off output source
finalNode = nil;
// Tear them down in reverse
converterNode = nil;
inputNode = nil;
inputNode = [[InputNode alloc] initWithController:self previous:nil];
if(!inputNode) return NO;
converterNode = [[ConverterNode alloc] initWithController:self previous:inputNode];
if(!converterNode) return NO;
finalNode = converterNode;
return YES;
}
- (BOOL)open:(NSURL *)url withOutputFormat:(AudioStreamBasicDescription)outputFormat withOutputConfig:(uint32_t)outputConfig withUserInfo:(id)userInfo withRGInfo:(NSDictionary *)rgi {
- (BOOL)open:(NSURL *)url withOutputFormat:(AudioStreamBasicDescription)outputFormat withUserInfo:(id)userInfo withRGInfo:(NSDictionary *)rgi {
[self setStreamURL:url];
[self setUserInfo:userInfo];
[self buildChain];
if (![self buildChain]) {
DLog(@"Couldn't build processing chain...");
return NO;
}
id<CogSource> source = [AudioSource audioSourceForURL:url];
DLog(@"Opening: %@", url);
@ -59,21 +73,9 @@
if(![inputNode openWithSource:source])
return NO;
NSDictionary *properties = [inputNode properties];
AudioStreamBasicDescription inputFormat = [inputNode nodeFormat];
uint32_t inputChannelConfig = 0;
if([properties valueForKey:@"channelConfig"])
inputChannelConfig = [[properties valueForKey:@"channelConfig"] unsignedIntValue];
outputFormat.mChannelsPerFrame = inputFormat.mChannelsPerFrame;
outputFormat.mBytesPerFrame = ((outputFormat.mBitsPerChannel + 7) / 8) * outputFormat.mChannelsPerFrame;
outputFormat.mBytesPerPacket = outputFormat.mBytesPerFrame * outputFormat.mFramesPerPacket;
outputConfig = inputChannelConfig;
if(![converterNode setupWithInputFormat:inputFormat withInputConfig:inputChannelConfig outputFormat:outputFormat outputConfig:outputConfig isLossless:[[properties valueForKey:@"encoding"] isEqualToString:@"lossless"]])
if(![self initConverter:outputFormat])
return NO;
[self initDownmixer];
[self setRGInfo:rgi];
@ -82,30 +84,20 @@
return YES;
}
- (BOOL)openWithInput:(InputNode *)i withOutputFormat:(AudioStreamBasicDescription)outputFormat withOutputConfig:(uint32_t)outputConfig withUserInfo:(id)userInfo withRGInfo:(NSDictionary *)rgi {
- (BOOL)openWithInput:(InputNode *)i withOutputFormat:(AudioStreamBasicDescription)outputFormat withUserInfo:(id)userInfo withRGInfo:(NSDictionary *)rgi {
DLog(@"New buffer chain!");
[self setUserInfo:userInfo];
[self buildChain];
if(![self buildChain]) {
DLog(@"Couldn't build processing chain...");
return NO;
}
if(![inputNode openWithDecoder:[i decoder]])
return NO;
NSDictionary *properties = [inputNode properties];
AudioStreamBasicDescription inputFormat = [inputNode nodeFormat];
uint32_t inputChannelConfig = 0;
if([properties valueForKey:@"channelConfig"])
inputChannelConfig = [[properties valueForKey:@"channelConfig"] unsignedIntValue];
outputFormat.mChannelsPerFrame = inputFormat.mChannelsPerFrame;
outputFormat.mBytesPerFrame = ((outputFormat.mBitsPerChannel + 7) / 8) * outputFormat.mChannelsPerFrame;
outputFormat.mBytesPerPacket = outputFormat.mBytesPerFrame * outputFormat.mFramesPerPacket;
outputConfig = inputChannelConfig;
DLog(@"Input Properties: %@", properties);
if(![converterNode setupWithInputFormat:inputFormat withInputConfig:inputChannelConfig outputFormat:outputFormat outputConfig:outputConfig isLossless:[[properties objectForKey:@"encoding"] isEqualToString:@"lossless"]])
if(![self initConverter:outputFormat])
return NO;
[self initDownmixer];
[self setRGInfo:rgi];
@ -113,18 +105,30 @@
}
- (BOOL)openWithDecoder:(id<CogDecoder>)decoder
withOutputFormat:(AudioStreamBasicDescription)outputFormat
withOutputConfig:(uint32_t)outputConfig
withOutputFormat:(AudioStreamBasicDescription)outputFormat
withUserInfo:(id)userInfo
withRGInfo:(NSDictionary *)rgi;
{
DLog(@"New buffer chain!");
[self setUserInfo:userInfo];
[self buildChain];
if(![self buildChain]) {
DLog(@"Couldn't build processing chain...");
return NO;
}
if(![inputNode openWithDecoder:decoder])
return NO;
if(![self initConverter:outputFormat])
return NO;
[self initDownmixer];
[self setRGInfo:rgi];
return YES;
}
- (BOOL)initConverter:(AudioStreamBasicDescription)outputFormat {
NSDictionary *properties = [inputNode properties];
DLog(@"Input Properties: %@", properties);
@ -138,16 +142,19 @@
outputFormat.mBytesPerFrame = ((outputFormat.mBitsPerChannel + 7) / 8) * outputFormat.mChannelsPerFrame;
outputFormat.mBytesPerPacket = outputFormat.mBytesPerFrame * outputFormat.mFramesPerPacket;
outputConfig = inputChannelConfig;
if(![converterNode setupWithInputFormat:inputFormat withInputConfig:inputChannelConfig outputFormat:outputFormat outputConfig:outputConfig isLossless:[[properties objectForKey:@"encoding"] isEqualToString:@"lossless"]])
if(![converterNode setupWithInputFormat:inputFormat withInputConfig:inputChannelConfig outputFormat:outputFormat isLossless:[[properties valueForKey:@"encoding"] isEqualToString:@"lossless"]])
return NO;
[self setRGInfo:rgi];
return YES;
}
- (void)initDownmixer {
AudioPlayer * audioPlayer = controller;
OutputNode *outputNode = [audioPlayer output];
DSPDownmixNode *downmixNode = [outputNode downmix];
[downmixNode setOutputFormat:[outputNode deviceFormat] withChannelConfig:[outputNode deviceChannelConfig]];
}
- (void)launchThreads {
DLog(@"Properties: %@", [inputNode properties]);
@ -175,8 +182,9 @@
- (void)dealloc {
[inputNode setShouldContinue:NO];
[[inputNode exitAtTheEndOfTheStream] signal];
[[inputNode semaphore] signal];
[[inputNode exitAtTheEndOfTheStream] wait]; // wait for decoder to be closed (see InputNode's -(void)process )
[[inputNode writeSemaphore] signal];
if(![inputNode threadExited])
[[inputNode exitAtTheEndOfTheStream] wait]; // wait for decoder to be closed (see InputNode's -(void)process )
DLog(@"Bufferchain dealloc");
}

View file

@ -8,24 +8,57 @@
#import <CoreAudio/CoreAudio.h>
#import <Foundation/Foundation.h>
#import "AudioChunk.h"
#import <CogAudio/AudioChunk.h>
#import "Semaphore.h"
#import <CogAudio/CogSemaphore.h>
NS_ASSUME_NONNULL_BEGIN
#define DSD_DECIMATE 1
@interface ChunkList : NSObject {
NSMutableArray<AudioChunk *> *chunkList;
double listDuration;
double listDurationRatioed;
double maxDuration;
BOOL inAdder;
BOOL inRemover;
BOOL inPeeker;
BOOL inMerger;
BOOL inConverter;
BOOL stopping;
// For format converter
void *inputBuffer;
size_t inputBufferSize;
#if DSD_DECIMATE
void **dsd2pcm;
size_t dsd2pcmCount;
int dsd2pcmLatency;
#endif
BOOL observersRegistered;
BOOL halveDSDVolume;
BOOL enableHDCD;
void *hdcd_decoder;
BOOL formatRead;
AudioStreamBasicDescription inputFormat;
AudioStreamBasicDescription floatFormat;
uint32_t inputChannelConfig;
BOOL inputLossless;
uint8_t *tempData;
size_t tempDataSize;
}
@property(readonly) double listDuration;
@property(readonly) double listDurationRatioed;
@property(readonly) double maxDuration;
- (id)initWithMaximumDuration:(double)duration;
@ -38,8 +71,16 @@ NS_ASSUME_NONNULL_BEGIN
- (void)addChunk:(AudioChunk *)chunk;
- (AudioChunk *)removeSamples:(size_t)maxFrameCount;
- (AudioChunk *)removeSamplesAsFloat32:(size_t)maxFrameCount;
- (BOOL)peekFormat:(nonnull AudioStreamBasicDescription *)format channelConfig:(nonnull uint32_t *)config;
- (BOOL)peekTimestamp:(nonnull double *)timestamp timeRatio:(nonnull double *)timeRatio;
// Helpers
- (AudioChunk *)removeAndMergeSamples:(size_t)maxFrameCount callBlock:(BOOL(NS_NOESCAPE ^ _Nonnull)(void))block;
- (AudioChunk *)removeAndMergeSamplesAsFloat32:(size_t)maxFrameCount callBlock:(BOOL(NS_NOESCAPE ^ _Nonnull)(void))block;
@end
NS_ASSUME_NONNULL_END

View file

@ -5,11 +5,371 @@
// Created by Christopher Snowhill on 2/5/22.
//
#import <Accelerate/Accelerate.h>
#import "ChunkList.h"
#import "hdcd_decode2.h"
#if !DSD_DECIMATE
#import "dsd2float.h"
#endif
#ifdef _DEBUG
#import "BadSampleCleaner.h"
#endif
static void *kChunkListContext = &kChunkListContext;
#if DSD_DECIMATE
/**
* DSD 2 PCM: Stage 1:
* Decimate by factor 8
* (one byte (8 samples) -> one float sample)
* The bits are processed from least signicifant to most signicicant.
* @author Sebastian Gesemann
*/
/**
* This is the 2nd half of an even order symmetric FIR
* lowpass filter (to be used on a signal sampled at 44100*64 Hz)
* Passband is 0-24 kHz (ripples +/- 0.025 dB)
* Stopband starts at 176.4 kHz (rejection: 170 dB)
* The overall gain is 2.0
*/
#define dsd2pcm_FILTER_COEFFS_COUNT 64
static const float dsd2pcm_FILTER_COEFFS[64] = {
0.09712411121659f, 0.09613438994044f, 0.09417884216316f, 0.09130441727307f,
0.08757947648990f, 0.08309142055179f, 0.07794369263673f, 0.07225228745463f,
0.06614191680338f, 0.05974199351302f, 0.05318259916599f, 0.04659059631228f,
0.04008603356890f, 0.03377897290478f, 0.02776684382775f, 0.02213240062966f,
0.01694232798846f, 0.01224650881275f, 0.00807793792573f, 0.00445323755944f,
0.00137370697215f, -0.00117318019994f, -0.00321193033831f, -0.00477694265140f,
-0.00591028841335f, -0.00665946056286f, -0.00707518873201f, -0.00720940203988f,
-0.00711340642819f, -0.00683632603227f, -0.00642384017266f, -0.00591723006715f,
-0.00535273320457f, -0.00476118922548f, -0.00416794965654f, -0.00359301524813f,
-0.00305135909510f, -0.00255339111833f, -0.00210551956895f, -0.00171076760278f,
-0.00136940723130f, -0.00107957856005f, -0.00083786862365f, -0.00063983084245f,
-0.00048043272086f, -0.00035442550015f, -0.00025663481039f, -0.00018217573430f,
-0.00012659899635f, -0.00008597726991f, -0.00005694188820f, -0.00003668060332f,
-0.00002290670286f, -0.00001380895679f, -0.00000799057558f, -0.00000440385083f,
-0.00000228567089f, -0.00000109760778f, -0.00000047286430f, -0.00000017129652f,
-0.00000004282776f, 0.00000000119422f, 0.00000000949179f, 0.00000000747450f
};
struct dsd2pcm_state {
/*
* This is the 2nd half of an even order symmetric FIR
* lowpass filter (to be used on a signal sampled at 44100*64 Hz)
* Passband is 0-24 kHz (ripples +/- 0.025 dB)
* Stopband starts at 176.4 kHz (rejection: 170 dB)
* The overall gain is 2.0
*/
/* These remain constant for the duration */
int FILT_LOOKUP_PARTS;
float *FILT_LOOKUP_TABLE;
uint8_t *REVERSE_BITS;
int FIFO_LENGTH;
int FIFO_OFS_MASK;
/* These are altered */
int *fifo;
int fpos;
};
static void dsd2pcm_free(void *);
static void dsd2pcm_reset(void *);
static void *dsd2pcm_alloc(void) {
struct dsd2pcm_state *state = (struct dsd2pcm_state *)calloc(1, sizeof(struct dsd2pcm_state));
float *FILT_LOOKUP_TABLE;
double *temp;
uint8_t *REVERSE_BITS;
if(!state)
return NULL;
state->FILT_LOOKUP_PARTS = (dsd2pcm_FILTER_COEFFS_COUNT + 7) / 8;
const int FILT_LOOKUP_PARTS = state->FILT_LOOKUP_PARTS;
// The current 128 tap FIR leads to an 8 KB lookup table
state->FILT_LOOKUP_TABLE = (float *)calloc(sizeof(float), FILT_LOOKUP_PARTS << 8);
if(!state->FILT_LOOKUP_TABLE)
goto fail;
FILT_LOOKUP_TABLE = state->FILT_LOOKUP_TABLE;
temp = (double *)calloc(sizeof(double), 0x100);
if(!temp)
goto fail;
for(int part = 0, sofs = 0, dofs = 0; part < FILT_LOOKUP_PARTS;) {
memset(temp, 0, 0x100 * sizeof(double));
for(int bit = 0, bitmask = 0x80; bit < 8 && sofs + bit < dsd2pcm_FILTER_COEFFS_COUNT;) {
double coeff = dsd2pcm_FILTER_COEFFS[sofs + bit];
for(int bite = 0; bite < 0x100; bite++) {
if((bite & bitmask) == 0) {
temp[bite] -= coeff;
} else {
temp[bite] += coeff;
}
}
bit++;
bitmask >>= 1;
}
for(int s = 0; s < 0x100;) {
FILT_LOOKUP_TABLE[dofs++] = (float)temp[s++];
}
part++;
sofs += 8;
}
free(temp);
{ // calculate FIFO stuff
int k = 1;
while(k < FILT_LOOKUP_PARTS * 2) k <<= 1;
state->FIFO_LENGTH = k;
state->FIFO_OFS_MASK = k - 1;
}
state->REVERSE_BITS = (uint8_t *)calloc(1, 0x100);
if(!state->REVERSE_BITS)
goto fail;
REVERSE_BITS = state->REVERSE_BITS;
for(int i = 0, j = 0; i < 0x100; i++) {
REVERSE_BITS[i] = (uint8_t)j;
// "reverse-increment" of j
for(int bitmask = 0x80;;) {
if(((j ^= bitmask) & bitmask) != 0) break;
if(bitmask == 1) break;
bitmask >>= 1;
}
}
state->fifo = (int *)calloc(sizeof(int), state->FIFO_LENGTH);
if(!state->fifo)
goto fail;
dsd2pcm_reset(state);
return (void *)state;
fail:
dsd2pcm_free(state);
return NULL;
}
static void *dsd2pcm_dup(void *_state) {
struct dsd2pcm_state *state = (struct dsd2pcm_state *)_state;
if(state) {
struct dsd2pcm_state *newstate = (struct dsd2pcm_state *)calloc(1, sizeof(struct dsd2pcm_state));
if(newstate) {
newstate->FILT_LOOKUP_PARTS = state->FILT_LOOKUP_PARTS;
newstate->FIFO_LENGTH = state->FIFO_LENGTH;
newstate->FIFO_OFS_MASK = state->FIFO_OFS_MASK;
newstate->fpos = state->fpos;
newstate->FILT_LOOKUP_TABLE = (float *)calloc(sizeof(float), state->FILT_LOOKUP_PARTS << 8);
if(!newstate->FILT_LOOKUP_TABLE)
goto fail;
memcpy(newstate->FILT_LOOKUP_TABLE, state->FILT_LOOKUP_TABLE, sizeof(float) * (state->FILT_LOOKUP_PARTS << 8));
newstate->REVERSE_BITS = (uint8_t *)calloc(1, 0x100);
if(!newstate->REVERSE_BITS)
goto fail;
memcpy(newstate->REVERSE_BITS, state->REVERSE_BITS, 0x100);
newstate->fifo = (int *)calloc(sizeof(int), state->FIFO_LENGTH);
if(!newstate->fifo)
goto fail;
memcpy(newstate->fifo, state->fifo, sizeof(int) * state->FIFO_LENGTH);
return (void *)newstate;
}
fail:
dsd2pcm_free(newstate);
return NULL;
}
return NULL;
}
static void dsd2pcm_free(void *_state) {
struct dsd2pcm_state *state = (struct dsd2pcm_state *)_state;
if(state) {
free(state->fifo);
free(state->REVERSE_BITS);
free(state->FILT_LOOKUP_TABLE);
free(state);
}
}
static void dsd2pcm_reset(void *_state) {
struct dsd2pcm_state *state = (struct dsd2pcm_state *)_state;
const int FILT_LOOKUP_PARTS = state->FILT_LOOKUP_PARTS;
int *fifo = state->fifo;
for(int i = 0; i < FILT_LOOKUP_PARTS; i++) {
fifo[i] = 0x55;
fifo[i + FILT_LOOKUP_PARTS] = 0xAA;
}
state->fpos = FILT_LOOKUP_PARTS;
}
static int dsd2pcm_latency(void *_state) {
struct dsd2pcm_state *state = (struct dsd2pcm_state *)_state;
if(state)
return state->FILT_LOOKUP_PARTS * 8;
else
return 0;
}
static void dsd2pcm_process(void *_state, const uint8_t *src, size_t sofs, size_t sinc, float *dest, size_t dofs, size_t dinc, size_t len) {
struct dsd2pcm_state *state = (struct dsd2pcm_state *)_state;
int bite1, bite2, temp;
float sample;
int *fifo = state->fifo;
const uint8_t *REVERSE_BITS = state->REVERSE_BITS;
const float *FILT_LOOKUP_TABLE = state->FILT_LOOKUP_TABLE;
const int FILT_LOOKUP_PARTS = state->FILT_LOOKUP_PARTS;
const int FIFO_OFS_MASK = state->FIFO_OFS_MASK;
int fpos = state->fpos;
while(len > 0) {
fifo[fpos] = REVERSE_BITS[fifo[fpos]] & 0xFF;
fifo[(fpos + FILT_LOOKUP_PARTS) & FIFO_OFS_MASK] = src[sofs] & 0xFF;
sofs += sinc;
temp = (fpos + 1) & FIFO_OFS_MASK;
sample = 0;
for(int k = 0, lofs = 0; k < FILT_LOOKUP_PARTS;) {
bite1 = fifo[(fpos - k) & FIFO_OFS_MASK];
bite2 = fifo[(temp + k) & FIFO_OFS_MASK];
sample += FILT_LOOKUP_TABLE[lofs + bite1] + FILT_LOOKUP_TABLE[lofs + bite2];
k++;
lofs += 0x100;
}
fpos = temp;
dest[dofs] = sample;
dofs += dinc;
len--;
}
state->fpos = fpos;
}
static void convert_dsd_to_f32(float *output, const uint8_t *input, size_t count, size_t channels, void **dsd2pcm) {
for(size_t channel = 0; channel < channels; ++channel) {
dsd2pcm_process(dsd2pcm[channel], input, channel, channels, output, channel, channels, count);
}
}
#else
static void convert_dsd_to_f32(float *output, const uint8_t *input, size_t count, size_t channels) {
const uint8_t *iptr = input;
float *optr = output;
for(size_t index = 0; index < count; ++index) {
for(size_t channel = 0; channel < channels; ++channel) {
uint8_t sample = *iptr++;
cblas_scopy(8, &dsd2float[sample][0], 1, optr++, (int)channels);
}
optr += channels * 7;
}
}
#endif
static void convert_u8_to_s16(int16_t *output, const uint8_t *input, size_t count) {
for(size_t i = 0; i < count; ++i) {
uint16_t sample = (input[i] << 8) | input[i];
sample ^= 0x8080;
output[i] = (int16_t)(sample);
}
}
static void convert_s8_to_s16(int16_t *output, const uint8_t *input, size_t count) {
for(size_t i = 0; i < count; ++i) {
uint16_t sample = (input[i] << 8) | input[i];
output[i] = (int16_t)(sample);
}
}
static void convert_u16_to_s16(int16_t *buffer, size_t count) {
for(size_t i = 0; i < count; ++i) {
buffer[i] ^= 0x8000;
}
}
static void convert_s16_to_hdcd_input(int32_t *output, const int16_t *input, size_t count) {
for(size_t i = 0; i < count; ++i) {
output[i] = input[i];
}
}
static void convert_s24_to_s32(int32_t *output, const uint8_t *input, size_t count) {
for(size_t i = 0; i < count; ++i) {
int32_t sample = (input[i * 3] << 8) | (input[i * 3 + 1] << 16) | (input[i * 3 + 2] << 24);
output[i] = sample;
}
}
static void convert_u24_to_s32(int32_t *output, const uint8_t *input, size_t count) {
for(size_t i = 0; i < count; ++i) {
int32_t sample = (input[i * 3] << 8) | (input[i * 3 + 1] << 16) | (input[i * 3 + 2] << 24);
output[i] = sample ^ 0x80000000;
}
}
static void convert_u32_to_s32(int32_t *buffer, size_t count) {
for(size_t i = 0; i < count; ++i) {
buffer[i] ^= 0x80000000;
}
}
static void convert_f64_to_f32(float *output, const double *input, size_t count) {
vDSP_vdpsp(input, 1, output, 1, count);
}
static void convert_be_to_le(uint8_t *buffer, size_t bitsPerSample, size_t bytes) {
size_t i;
bitsPerSample = (bitsPerSample + 7) / 8;
switch(bitsPerSample) {
case 2:
for(i = 0; i < bytes; i += 2) {
*(int16_t *)buffer = __builtin_bswap16(*(int16_t *)buffer);
buffer += 2;
}
break;
case 3: {
union {
vDSP_int24 int24;
uint32_t int32;
} intval;
intval.int32 = 0;
for(i = 0; i < bytes; i += 3) {
intval.int24 = *(vDSP_int24 *)buffer;
intval.int32 = __builtin_bswap32(intval.int32 << 8);
*(vDSP_int24 *)buffer = intval.int24;
buffer += 3;
}
} break;
case 4:
for(i = 0; i < bytes; i += 4) {
*(uint32_t *)buffer = __builtin_bswap32(*(uint32_t *)buffer);
buffer += 4;
}
break;
case 8:
for(i = 0; i < bytes; i += 8) {
*(uint64_t *)buffer = __builtin_bswap64(*(uint64_t *)buffer);
buffer += 8;
}
break;
}
}
@implementation ChunkList
@synthesize listDuration;
@synthesize listDurationRatioed;
@synthesize maxDuration;
- (id)initWithMaximumDuration:(double)duration {
@ -18,28 +378,97 @@
if(self) {
chunkList = [[NSMutableArray alloc] init];
listDuration = 0.0;
listDurationRatioed = 0.0;
maxDuration = duration;
inAdder = NO;
inRemover = NO;
inPeeker = NO;
inMerger = NO;
inConverter = NO;
stopping = NO;
formatRead = NO;
inputBuffer = NULL;
inputBufferSize = 0;
#if DSD_DECIMATE
dsd2pcm = NULL;
dsd2pcmCount = 0;
dsd2pcmLatency = 0;
#endif
observersRegistered = NO;
}
return self;
}
- (void)addObservers {
if(!observersRegistered) {
halveDSDVolume = NO;
enableHDCD = NO;
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.halveDSDVolume" options:(NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew) context:kChunkListContext];
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.enableHDCD" options:(NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew) context:kChunkListContext];
observersRegistered = YES;
}
}
- (void)removeObservers {
if(observersRegistered) {
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.halveDSDVolume" context:kChunkListContext];
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.enableHDCD" context:kChunkListContext];
observersRegistered = NO;
}
}
- (void)dealloc {
stopping = YES;
while(inAdder || inRemover || inPeeker) {
while(inAdder || inRemover || inPeeker || inMerger || inConverter) {
usleep(500);
}
[self removeObservers];
if(hdcd_decoder) {
free(hdcd_decoder);
hdcd_decoder = NULL;
}
#if DSD_DECIMATE
if(dsd2pcm && dsd2pcmCount) {
for(size_t i = 0; i < dsd2pcmCount; ++i) {
dsd2pcm_free(dsd2pcm[i]);
dsd2pcm[i] = NULL;
}
free(dsd2pcm);
dsd2pcm = NULL;
}
#endif
if(tempData) {
free(tempData);
}
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if(context != kChunkListContext) {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
return;
}
if([keyPath isEqualToString:@"values.halveDSDVolume"]) {
halveDSDVolume = [[[NSUserDefaultsController sharedUserDefaultsController] defaults] boolForKey:@"halveDSDVolume"];
} else if([keyPath isEqualToString:@"values.enableHDCD"]) {
enableHDCD = [[[NSUserDefaultsController sharedUserDefaultsController] defaults] boolForKey:@"enableHDCD"];
}
}
- (void)reset {
@synchronized(chunkList) {
[chunkList removeAllObjects];
listDuration = 0.0;
listDurationRatioed = 0.0;
}
}
@ -50,7 +479,9 @@
}
- (BOOL)isFull {
return (maxDuration - listDuration) < 0.05;
@synchronized (chunkList) {
return (maxDuration - listDuration) < 0.001;
}
}
- (void)addChunk:(AudioChunk *)chunk {
@ -59,10 +490,12 @@
inAdder = YES;
const double chunkDuration = [chunk duration];
const double chunkDurationRatioed = [chunk durationRatioed];
@synchronized(chunkList) {
[chunkList addObject:chunk];
listDuration += chunkDuration;
listDurationRatioed += chunkDurationRatioed;
}
inAdder = NO;
@ -83,30 +516,461 @@
if([chunk frameCount] <= maxFrameCount) {
[chunkList removeObjectAtIndex:0];
listDuration -= [chunk duration];
listDurationRatioed -= [chunk durationRatioed];
inRemover = NO;
return chunk;
}
double streamTimestamp = [chunk streamTimestamp];
NSData *removedData = [chunk removeSamples:maxFrameCount];
AudioChunk *ret = [[AudioChunk alloc] init];
[ret setFormat:[chunk format]];
[ret setChannelConfig:[chunk channelConfig]];
[ret assignSamples:[removedData bytes] frameCount:maxFrameCount];
[ret setLossless:[chunk lossless]];
[ret setStreamTimestamp:streamTimestamp];
[ret setStreamTimeRatio:[chunk streamTimeRatio]];
[ret assignData:removedData];
listDuration -= [ret duration];
listDurationRatioed -= [ret durationRatioed];
inRemover = NO;
return ret;
}
}
- (AudioChunk *)removeSamplesAsFloat32:(size_t)maxFrameCount {
if(stopping) {
return [[AudioChunk alloc] init];
}
@synchronized (chunkList) {
inRemover = YES;
if(![chunkList count]) {
inRemover = NO;
return [[AudioChunk alloc] init];
}
AudioChunk *chunk = [chunkList objectAtIndex:0];
#if !DSD_DECIMATE
AudioStreamBasicDescription asbd = [chunk format];
if(asbd.mBitsPerChannel == 1) {
maxFrameCount /= 8;
}
#endif
if([chunk frameCount] <= maxFrameCount) {
[chunkList removeObjectAtIndex:0];
listDuration -= [chunk duration];
listDurationRatioed -= [chunk durationRatioed];
inRemover = NO;
return [self convertChunk:chunk];
}
double streamTimestamp = [chunk streamTimestamp];
NSData *removedData = [chunk removeSamples:maxFrameCount];
AudioChunk *ret = [[AudioChunk alloc] init];
[ret setFormat:[chunk format]];
[ret setChannelConfig:[chunk channelConfig]];
[ret setLossless:[chunk lossless]];
[ret setStreamTimestamp:streamTimestamp];
[ret setStreamTimeRatio:[chunk streamTimeRatio]];
[ret assignData:removedData];
listDuration -= [ret duration];
listDurationRatioed -= [ret durationRatioed];
inRemover = NO;
return [self convertChunk:ret];
}
}
- (AudioChunk *)removeAndMergeSamples:(size_t)maxFrameCount callBlock:(BOOL(NS_NOESCAPE ^ _Nonnull)(void))block {
if(stopping) {
return [[AudioChunk alloc] init];
}
inMerger = YES;
BOOL formatSet = NO;
AudioStreamBasicDescription currentFormat;
uint32_t currentChannelConfig = 0;
double streamTimestamp = 0.0;
double streamTimeRatio = 1.0;
BOOL blocked = NO;
while(![self peekTimestamp:&streamTimestamp timeRatio:&streamTimeRatio]) {
if((blocked = block())) {
break;
}
}
if(blocked) {
inMerger = NO;
return [[AudioChunk alloc] init];
}
AudioChunk *chunk;
size_t totalFrameCount = 0;
AudioChunk *outputChunk = [[AudioChunk alloc] init];
[outputChunk setStreamTimestamp:streamTimestamp];
[outputChunk setStreamTimeRatio:streamTimeRatio];
while(!stopping && totalFrameCount < maxFrameCount) {
AudioStreamBasicDescription newFormat;
uint32_t newChannelConfig;
if(![self peekFormat:&newFormat channelConfig:&newChannelConfig]) {
if(block()) {
break;
}
continue;
}
if(formatSet &&
(memcmp(&newFormat, &currentFormat, sizeof(newFormat)) != 0 ||
newChannelConfig != currentChannelConfig)) {
break;
} else if(!formatSet) {
[outputChunk setFormat:newFormat];
[outputChunk setChannelConfig:newChannelConfig];
currentFormat = newFormat;
currentChannelConfig = newChannelConfig;
formatSet = YES;
}
chunk = [self removeSamples:maxFrameCount - totalFrameCount];
if(!chunk || ![chunk frameCount]) {
if(block()) {
break;
}
continue;
}
if([chunk isHDCD]) {
[outputChunk setHDCD];
}
size_t frameCount = [chunk frameCount];
NSData *sampleData = [chunk removeSamples:frameCount];
[outputChunk assignData:sampleData];
totalFrameCount += frameCount;
}
if(!totalFrameCount) {
inMerger = NO;
return [[AudioChunk alloc] init];
}
inMerger = NO;
return outputChunk;
}
- (AudioChunk *)removeAndMergeSamplesAsFloat32:(size_t)maxFrameCount callBlock:(BOOL(NS_NOESCAPE ^ _Nonnull)(void))block {
AudioChunk *ret = [self removeAndMergeSamples:maxFrameCount callBlock:block];
return [self convertChunk:ret];
}
- (AudioChunk *)convertChunk:(AudioChunk *)inChunk {
if(stopping) return [[AudioChunk alloc] init];
inConverter = YES;
AudioStreamBasicDescription chunkFormat = [inChunk format];
if(![inChunk frameCount] ||
(chunkFormat.mFormatFlags == kAudioFormatFlagsNativeFloatPacked &&
chunkFormat.mBitsPerChannel == 32)) {
inConverter = NO;
return inChunk;
}
uint32_t chunkConfig = [inChunk channelConfig];
BOOL chunkLossless = [inChunk lossless];
if(!formatRead || memcmp(&chunkFormat, &inputFormat, sizeof(chunkFormat)) != 0 ||
chunkConfig != inputChannelConfig || chunkLossless != inputLossless) {
formatRead = YES;
inputFormat = chunkFormat;
inputChannelConfig = chunkConfig;
inputLossless = chunkLossless;
BOOL isFloat = !!(inputFormat.mFormatFlags & kAudioFormatFlagIsFloat);
if((!isFloat && !(inputFormat.mBitsPerChannel >= 1 && inputFormat.mBitsPerChannel <= 32)) || (isFloat && !(inputFormat.mBitsPerChannel == 32 || inputFormat.mBitsPerChannel == 64))) {
inConverter = NO;
return [[AudioChunk alloc] init];
}
// These are really placeholders, as we're doing everything internally now
if(inputLossless &&
inputFormat.mBitsPerChannel == 16 &&
inputFormat.mChannelsPerFrame == 2 &&
inputFormat.mSampleRate == 44100) {
// possibly HDCD, run through decoder
[self addObservers];
if(hdcd_decoder) {
free(hdcd_decoder);
hdcd_decoder = NULL;
}
hdcd_decoder = calloc(1, sizeof(hdcd_state_stereo_t));
hdcd_reset_stereo((hdcd_state_stereo_t *)hdcd_decoder, 44100);
}
floatFormat = inputFormat;
floatFormat.mFormatFlags = kAudioFormatFlagsNativeFloatPacked;
floatFormat.mBitsPerChannel = 32;
floatFormat.mBytesPerFrame = (32 / 8) * floatFormat.mChannelsPerFrame;
floatFormat.mBytesPerPacket = floatFormat.mBytesPerFrame * floatFormat.mFramesPerPacket;
#if DSD_DECIMATE
if(inputFormat.mBitsPerChannel == 1) {
// Decimate this for speed
floatFormat.mSampleRate *= 1.0 / 8.0;
if(dsd2pcm && dsd2pcmCount) {
for(size_t i = 0; i < dsd2pcmCount; ++i) {
dsd2pcm_free(dsd2pcm[i]);
dsd2pcm[i] = NULL;
}
free(dsd2pcm);
dsd2pcm = NULL;
}
dsd2pcmCount = floatFormat.mChannelsPerFrame;
dsd2pcm = (void **)calloc(dsd2pcmCount, sizeof(void *));
dsd2pcm[0] = dsd2pcm_alloc();
dsd2pcmLatency = dsd2pcm_latency(dsd2pcm[0]);
for(size_t i = 1; i < dsd2pcmCount; ++i) {
dsd2pcm[i] = dsd2pcm_dup(dsd2pcm[0]);
}
}
#endif
}
NSUInteger samplesRead = [inChunk frameCount];
if(!samplesRead) {
inConverter = NO;
return [[AudioChunk alloc] init];
}
BOOL isFloat = !!(inputFormat.mFormatFlags & kAudioFormatFlagIsFloat);
BOOL isUnsigned = !isFloat && !(inputFormat.mFormatFlags & kAudioFormatFlagIsSignedInteger);
size_t bitsPerSample = inputFormat.mBitsPerChannel;
BOOL isBigEndian = !!(inputFormat.mFormatFlags & kAudioFormatFlagIsBigEndian);
double streamTimestamp = [inChunk streamTimestamp];
NSData *inputData = [inChunk removeSamples:samplesRead];
#if DSD_DECIMATE
const size_t sizeFactor = 3;
#else
const size_t sizeFactor = (bitsPerSample == 1) ? 9 : 3;
#endif
size_t newSize = samplesRead * floatFormat.mBytesPerPacket * sizeFactor + 64;
if(!tempData || tempDataSize < newSize)
tempData = realloc(tempData, tempDataSize = newSize); // Either two buffers plus padding, and/or double precision in case of endian flip
// double buffer system, with alignment
const size_t buffer_adder_base = (samplesRead * floatFormat.mBytesPerPacket + 31) & ~31;
NSUInteger bytesReadFromInput = samplesRead * inputFormat.mBytesPerPacket;
uint8_t *inputBuffer = (uint8_t *)[inputData bytes];
BOOL inputChanged = NO;
BOOL hdcdSustained = NO;
if(bytesReadFromInput && isBigEndian) {
// Time for endian swap!
memcpy(&tempData[0], [inputData bytes], bytesReadFromInput);
convert_be_to_le((uint8_t *)(&tempData[0]), inputFormat.mBitsPerChannel, bytesReadFromInput);
inputBuffer = &tempData[0];
inputChanged = YES;
}
if(bytesReadFromInput && isFloat && bitsPerSample == 64) {
// Time for precision loss from weird inputs
const size_t buffer_adder = (inputBuffer == &tempData[0]) ? buffer_adder_base * 2 : 0;
samplesRead = bytesReadFromInput / sizeof(double);
convert_f64_to_f32((float *)(&tempData[buffer_adder]), (const double *)inputBuffer, samplesRead);
bytesReadFromInput = samplesRead * sizeof(float);
inputBuffer = &tempData[buffer_adder];
inputChanged = YES;
bitsPerSample = 32;
}
if(bytesReadFromInput && !isFloat) {
float gain = 1.0;
if(bitsPerSample == 1) {
const size_t buffer_adder = (inputBuffer == &tempData[0]) ? buffer_adder_base : 0;
samplesRead = bytesReadFromInput / inputFormat.mBytesPerPacket;
convert_dsd_to_f32((float *)(&tempData[buffer_adder]), (const uint8_t *)inputBuffer, samplesRead, inputFormat.mChannelsPerFrame
#if DSD_DECIMATE
,
dsd2pcm
#endif
);
#if !DSD_DECIMATE
samplesRead *= 8;
#endif
bitsPerSample = 32;
bytesReadFromInput = samplesRead * floatFormat.mBytesPerPacket;
isFloat = YES;
inputBuffer = &tempData[buffer_adder];
inputChanged = YES;
[self addObservers];
#if DSD_DECIMATE
if(halveDSDVolume) {
float scaleFactor = 2.0f;
vDSP_vsdiv((float *)inputBuffer, 1, &scaleFactor, (float *)inputBuffer, 1, bytesReadFromInput / sizeof(float));
}
#else
if(!halveDSDVolume) {
float scaleFactor = 2.0f;
vDSP_vsmul((float *)inputBuffer, 1, &scaleFactor, (float *)inputBuffer, 1, bytesReadFromInput / sizeof(float));
}
#endif
} else if(bitsPerSample <= 8) {
samplesRead = bytesReadFromInput;
const size_t buffer_adder = (inputBuffer == &tempData[0]) ? buffer_adder_base : 0;
if(!isUnsigned)
convert_s8_to_s16((int16_t *)(&tempData[buffer_adder]), (const uint8_t *)inputBuffer, samplesRead);
else
convert_u8_to_s16((int16_t *)(&tempData[buffer_adder]), (const uint8_t *)inputBuffer, samplesRead);
bitsPerSample = 16;
bytesReadFromInput = samplesRead * 2;
isUnsigned = NO;
inputBuffer = &tempData[buffer_adder];
inputChanged = YES;
}
if(hdcd_decoder) { // implied bits per sample is 16, produces 32 bit int scale
samplesRead = bytesReadFromInput / 2;
const size_t buffer_adder = (inputBuffer == &tempData[0]) ? buffer_adder_base : 0;
if(isUnsigned) {
if(!inputChanged) {
memcpy(&tempData[buffer_adder], inputBuffer, samplesRead * 2);
inputBuffer = &tempData[buffer_adder];
inputChanged = YES;
}
convert_u16_to_s16((int16_t *)inputBuffer, samplesRead);
isUnsigned = NO;
}
const size_t buffer_adder2 = (inputBuffer == &tempData[0]) ? buffer_adder_base : 0;
convert_s16_to_hdcd_input((int32_t *)(&tempData[buffer_adder2]), (int16_t *)inputBuffer, samplesRead);
hdcd_process_stereo((hdcd_state_stereo_t *)hdcd_decoder, (int32_t *)(&tempData[buffer_adder2]), (int)(samplesRead / 2));
if(((hdcd_state_stereo_t *)hdcd_decoder)->channel[0].sustain &&
((hdcd_state_stereo_t *)hdcd_decoder)->channel[1].sustain) {
hdcdSustained = YES;
}
if(enableHDCD) {
gain = 2.0;
bitsPerSample = 32;
bytesReadFromInput = samplesRead * 4;
isUnsigned = NO;
inputBuffer = &tempData[buffer_adder2];
inputChanged = YES;
} else {
// Discard the output of the decoder and process again
goto process16bit;
}
} else if(bitsPerSample <= 16) {
process16bit:
samplesRead = bytesReadFromInput / 2;
const size_t buffer_adder = (inputBuffer == &tempData[0]) ? buffer_adder_base : 0;
if(isUnsigned) {
if(!inputChanged) {
memcpy(&tempData[buffer_adder], inputBuffer, samplesRead * 2);
inputBuffer = &tempData[buffer_adder];
inputChanged = YES;
}
convert_u16_to_s16((int16_t *)inputBuffer, samplesRead);
}
const size_t buffer_adder2 = (inputBuffer == &tempData[0]) ? buffer_adder_base : 0;
vDSP_vflt16((const short *)inputBuffer, 1, (float *)(&tempData[buffer_adder2]), 1, samplesRead);
float scale = 1ULL << 15;
vDSP_vsdiv((const float *)(&tempData[buffer_adder2]), 1, &scale, (float *)(&tempData[buffer_adder2]), 1, samplesRead);
bitsPerSample = 32;
bytesReadFromInput = samplesRead * sizeof(float);
isUnsigned = NO;
isFloat = YES;
inputBuffer = &tempData[buffer_adder2];
inputChanged = YES;
} else if(bitsPerSample <= 24) {
const size_t buffer_adder = (inputBuffer == &tempData[0]) ? buffer_adder_base : 0;
samplesRead = bytesReadFromInput / 3;
if(isUnsigned)
convert_u24_to_s32((int32_t *)(&tempData[buffer_adder]), (uint8_t *)inputBuffer, samplesRead);
else
convert_s24_to_s32((int32_t *)(&tempData[buffer_adder]), (uint8_t *)inputBuffer, samplesRead);
bitsPerSample = 32;
bytesReadFromInput = samplesRead * 4;
isUnsigned = NO;
inputBuffer = &tempData[buffer_adder];
inputChanged = YES;
}
if(!isFloat && bitsPerSample <= 32) {
samplesRead = bytesReadFromInput / 4;
if(isUnsigned) {
if(!inputChanged) {
memcpy(&tempData[0], inputBuffer, bytesReadFromInput);
inputBuffer = &tempData[0];
}
convert_u32_to_s32((int32_t *)inputBuffer, samplesRead);
}
const size_t buffer_adder = (inputBuffer == &tempData[0]) ? buffer_adder_base : 0; // vDSP functions expect aligned to four elements
vDSP_vflt32((const int *)inputBuffer, 1, (float *)(&tempData[buffer_adder]), 1, samplesRead);
float scale = (1ULL << 31) / gain;
vDSP_vsdiv((const float *)(&tempData[buffer_adder]), 1, &scale, (float *)(&tempData[buffer_adder]), 1, samplesRead);
bitsPerSample = 32;
bytesReadFromInput = samplesRead * sizeof(float);
isUnsigned = NO;
isFloat = YES;
inputBuffer = &tempData[buffer_adder];
}
#ifdef _DEBUG
[BadSampleCleaner cleanSamples:(float *)inputBuffer
amount:bytesReadFromInput / sizeof(float)
location:@"post int to float conversion"];
#endif
}
AudioChunk *outChunk = [[AudioChunk alloc] init];
[outChunk setFormat:floatFormat];
[outChunk setChannelConfig:inputChannelConfig];
[outChunk setLossless:inputLossless];
[outChunk setStreamTimestamp:streamTimestamp];
[outChunk setStreamTimeRatio:[inChunk streamTimeRatio]];
if(hdcdSustained) [outChunk setHDCD];
[outChunk assignSamples:inputBuffer frameCount:bytesReadFromInput / floatFormat.mBytesPerPacket];
inConverter = NO;
return outChunk;
}
- (BOOL)peekFormat:(AudioStreamBasicDescription *)format channelConfig:(uint32_t *)config {
if(stopping) return NO;
inPeeker = YES;
@synchronized(chunkList) {
if([chunkList count]) {
AudioChunk *chunk = [chunkList objectAtIndex:0];
*format = [chunk format];
*config = [chunk channelConfig];
inPeeker = NO;
return YES;
}
}
inPeeker = NO;
return NO;
}
- (BOOL)peekTimestamp:(double *)timestamp timeRatio:(double *)timeRatio {
if(stopping) return NO;
inPeeker = YES;
@synchronized (chunkList) {
if([chunkList count]) {
AudioChunk *chunk = [chunkList objectAtIndex:0];
*timestamp = [chunk streamTimestamp];
*timeRatio = [chunk streamTimeRatio];
inPeeker = NO;
return YES;
}
}
*timestamp = 0.0;
*timeRatio = 1.0;
inPeeker = NO;
return NO;
}

View file

@ -12,21 +12,21 @@
#import <AudioUnit/AudioUnit.h>
#import <CoreAudio/AudioHardware.h>
#import "Node.h"
#import <CogAudio/soxr.h>
#import "HeadphoneFilter.h"
#define DSD_DECIMATE 1
#import <CogAudio/Node.h>
@interface ConverterNode : Node {
NSDictionary *rgInfo;
void *_r8bstate;
soxr_t soxr;
void *inputBuffer;
size_t inputBufferSize;
size_t inpSize, inpOffset;
double streamTimestamp, streamTimeRatio;
BOOL stopping;
BOOL convertEntered;
BOOL paused;
@ -37,63 +37,52 @@
unsigned int N_samples_to_add_;
unsigned int N_samples_to_drop_;
unsigned int is_preextrapolated_;
unsigned int is_postextrapolated_;
BOOL is_preextrapolated_;
int is_postextrapolated_;
int latencyEaten;
int latencyEatenPost;
double sampleRatio;
BOOL observersAdded;
float volumeScale;
void *floatBuffer;
size_t floatBufferSize;
size_t floatSize, floatOffset;
void *extrapolateBuffer;
size_t extrapolateBufferSize;
#if DSD_DECIMATE
void **dsd2pcm;
size_t dsd2pcmCount;
int dsd2pcmLatency;
#endif
BOOL rememberedLossless;
AudioStreamBasicDescription inputFormat;
AudioStreamBasicDescription floatFormat;
AudioStreamBasicDescription dmFloatFormat; // downmixed/upmixed float format
AudioStreamBasicDescription outputFormat;
uint32_t inputChannelConfig;
uint32_t outputChannelConfig;
BOOL streamFormatChanged;
AudioStreamBasicDescription newInputFormat;
uint32_t newInputChannelConfig;
AudioChunk *lastChunkIn;
void *hdcd_decoder;
HeadphoneFilter *hFilter;
}
@property AudioStreamBasicDescription inputFormat;
- (id)initWithController:(id)c previous:(id)p;
- (BOOL)setupWithInputFormat:(AudioStreamBasicDescription)inputFormat withInputConfig:(uint32_t)inputConfig outputFormat:(AudioStreamBasicDescription)outputFormat outputConfig:(uint32_t)outputConfig isLossless:(BOOL)lossless;
- (BOOL)setupWithInputFormat:(AudioStreamBasicDescription)inputFormat withInputConfig:(uint32_t)inputConfig outputFormat:(AudioStreamBasicDescription)outputFormat isLossless:(BOOL)lossless;
- (void)cleanUp;
- (BOOL)paused;
- (void)process;
- (int)convert:(void *)dest amount:(int)amount;
- (AudioChunk *)convert;
- (void)setRGInfo:(NSDictionary *)rgi;
- (void)setOutputFormat:(AudioStreamBasicDescription)format outputConfig:(uint32_t)outputConfig;
- (void)setOutputFormat:(AudioStreamBasicDescription)outputFormat;
- (void)inputFormatDidChange:(AudioStreamBasicDescription)format inputConfig:(uint32_t)inputConfig;

564
Audio/Chain/ConverterNode.m Normal file
View file

@ -0,0 +1,564 @@
//
// ConverterNode.m
// Cog
//
// Created by Zaphod Beeblebrox on 8/2/05.
// Copyright 2005 __MyCompanyName__. All rights reserved.
//
#import <Accelerate/Accelerate.h>
#import <Foundation/Foundation.h>
#import "ConverterNode.h"
#import "BufferChain.h"
#import "OutputNode.h"
#import "Logging.h"
#import "lpc.h"
#import "util.h"
#ifdef _DEBUG
#import "BadSampleCleaner.h"
#endif
void PrintStreamDesc(AudioStreamBasicDescription *inDesc) {
if(!inDesc) {
DLog(@"Can't print a NULL desc!\n");
return;
}
DLog(@"- - - - - - - - - - - - - - - - - - - -\n");
DLog(@" Sample Rate:%f\n", inDesc->mSampleRate);
DLog(@" Format ID:%s\n", (char *)&inDesc->mFormatID);
DLog(@" Format Flags:%X\n", inDesc->mFormatFlags);
DLog(@" Bytes per Packet:%d\n", inDesc->mBytesPerPacket);
DLog(@" Frames per Packet:%d\n", inDesc->mFramesPerPacket);
DLog(@" Bytes per Frame:%d\n", inDesc->mBytesPerFrame);
DLog(@" Channels per Frame:%d\n", inDesc->mChannelsPerFrame);
DLog(@" Bits per Channel:%d\n", inDesc->mBitsPerChannel);
DLog(@"- - - - - - - - - - - - - - - - - - - -\n");
}
@implementation ConverterNode
static void *kConverterNodeContext = &kConverterNodeContext;
@synthesize inputFormat;
- (id)initWithController:(id)c previous:(id)p {
self = [super initWithController:c previous:p];
if(self) {
rgInfo = nil;
soxr = 0;
inputBuffer = NULL;
inputBufferSize = 0;
floatBuffer = NULL;
floatBufferSize = 0;
stopping = NO;
convertEntered = NO;
paused = NO;
skipResampler = YES;
extrapolateBuffer = NULL;
extrapolateBufferSize = 0;
#ifdef LOG_CHAINS
[self initLogFiles];
#endif
}
return self;
}
- (void)addObservers {
if(!observersAdded) {
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.volumeScaling" options:(NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew) context:kConverterNodeContext];
observersAdded = YES;
}
}
- (void)removeObservers {
if(observersAdded) {
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.volumeScaling" context:kConverterNodeContext];
observersAdded = NO;
}
}
void scale_by_volume(float *buffer, size_t count, float volume) {
if(volume != 1.0) {
size_t unaligned = (uintptr_t)buffer & 15;
if(unaligned) {
size_t count_unaligned = (16 - unaligned) / sizeof(float);
while(count > 0 && count_unaligned > 0) {
*buffer++ *= volume;
count_unaligned--;
count--;
}
}
if(count) {
vDSP_vsmul(buffer, 1, &volume, buffer, 1, count);
}
}
}
- (BOOL)paused {
return paused;
}
- (void)process {
// Removed endOfStream check from here, since we want to be able to flush the converter
// when the end of stream is reached. Convert function instead processes what it can,
// and returns 0 samples when it has nothing more to process at the end of stream.
while([self shouldContinue] == YES) {
while(paused) {
usleep(500);
}
@autoreleasepool {
AudioChunk *chunk = nil;
chunk = [self convert];
if(!chunk || ![chunk frameCount]) {
if([[previousNode buffer] isEmpty] && [previousNode endOfStream] == YES) {
endOfStream = YES;
break;
}
if(paused || !streamFormatChanged) {
continue;
}
usleep(500);
} else {
[self writeChunk:chunk];
chunk = nil;
}
if(streamFormatChanged) {
[self cleanUp];
[self setupWithInputFormat:newInputFormat withInputConfig:newInputChannelConfig outputFormat:self->outputFormat isLossless:rememberedLossless];
}
}
}
endOfStream = YES;
}
- (AudioChunk *)convert {
UInt32 ioNumberPackets;
if(stopping)
return 0;
convertEntered = YES;
if(stopping || [self shouldContinue] == NO) {
convertEntered = NO;
return nil;
}
if(inpOffset == inpSize) {
streamTimestamp = 0.0;
streamTimeRatio = 1.0;
if(![self peekTimestamp:&streamTimestamp timeRatio:&streamTimeRatio]) {
convertEntered = NO;
return nil;
}
}
while(inpOffset == inpSize) {
// Approximately the most we want on input
ioNumberPackets = 4096;
size_t newSize = ioNumberPackets * floatFormat.mBytesPerPacket;
if(!inputBuffer || inputBufferSize < newSize)
inputBuffer = realloc(inputBuffer, inputBufferSize = newSize);
ssize_t amountToWrite = ioNumberPackets * floatFormat.mBytesPerPacket;
ssize_t bytesReadFromInput = 0;
while(bytesReadFromInput < amountToWrite && !stopping && !paused && !streamFormatChanged && [self shouldContinue] == YES && !([[previousNode buffer] isEmpty] && [previousNode endOfStream] == YES)) {
AudioStreamBasicDescription inf;
uint32_t config;
if([self peekFormat:&inf channelConfig:&config]) {
if(config != inputChannelConfig || memcmp(&inf, &inputFormat, sizeof(inf)) != 0) {
if(inputChannelConfig == 0 && memcmp(&inf, &inputFormat, sizeof(inf)) == 0) {
inputChannelConfig = config;
continue;
} else {
newInputFormat = inf;
newInputChannelConfig = config;
streamFormatChanged = YES;
break;
}
}
}
AudioChunk *chunk = [self readChunkAsFloat32:((amountToWrite - bytesReadFromInput) / floatFormat.mBytesPerPacket)];
inf = [chunk format];
size_t frameCount = [chunk frameCount];
config = [chunk channelConfig];
size_t bytesRead = frameCount * inf.mBytesPerPacket;
if(frameCount) {
NSData *samples = [chunk removeSamples:frameCount];
memcpy(((uint8_t *)inputBuffer) + bytesReadFromInput, [samples bytes], bytesRead);
if([chunk isHDCD]) {
[controller sustainHDCD];
}
}
bytesReadFromInput += bytesRead;
if(!frameCount) {
usleep(500);
}
}
if(!bytesReadFromInput) {
convertEntered = NO;
return nil;
}
if(stopping || paused || streamFormatChanged || [self shouldContinue] == NO || ([[previousNode buffer] isEmpty] && [previousNode endOfStream] == YES)) {
if(!skipResampler) {
if(!is_postextrapolated_) {
is_postextrapolated_ = 1;
}
} else {
is_postextrapolated_ = 3;
}
}
// Extrapolate start
if(!skipResampler && !is_preextrapolated_) {
size_t inputSamples = bytesReadFromInput / floatFormat.mBytesPerPacket;
size_t prime = MIN(inputSamples, PRIME_LEN_);
size_t _N_samples_to_add_ = N_samples_to_add_;
size_t newSize = _N_samples_to_add_ * floatFormat.mBytesPerPacket;
newSize += bytesReadFromInput;
if(newSize > inputBufferSize) {
inputBuffer = realloc(inputBuffer, inputBufferSize = newSize * 3);
}
memmove(inputBuffer + _N_samples_to_add_ * floatFormat.mBytesPerPacket, inputBuffer, bytesReadFromInput);
lpc_extrapolate_bkwd(inputBuffer + _N_samples_to_add_ * floatFormat.mBytesPerPacket, inputSamples, prime, floatFormat.mChannelsPerFrame, LPC_ORDER, _N_samples_to_add_, &extrapolateBuffer, &extrapolateBufferSize);
bytesReadFromInput += _N_samples_to_add_ * floatFormat.mBytesPerPacket;
latencyEaten = N_samples_to_drop_;
is_preextrapolated_ = YES;
}
if(is_postextrapolated_ == 1) {
size_t inputSamples = bytesReadFromInput / floatFormat.mBytesPerPacket;
size_t prime = MIN(inputSamples, PRIME_LEN_);
size_t _N_samples_to_add_ = N_samples_to_add_;
size_t newSize = bytesReadFromInput;
newSize += _N_samples_to_add_ * floatFormat.mBytesPerPacket;
if(newSize > inputBufferSize) {
inputBuffer = realloc(inputBuffer, inputBufferSize = newSize * 3);
}
lpc_extrapolate_fwd(inputBuffer, inputSamples, prime, floatFormat.mChannelsPerFrame, LPC_ORDER, _N_samples_to_add_, &extrapolateBuffer, &extrapolateBufferSize);
bytesReadFromInput += _N_samples_to_add_ * floatFormat.mBytesPerPacket;
latencyEatenPost = N_samples_to_drop_;
is_postextrapolated_ = 2;
} else if(is_postextrapolated_ == 3) {
latencyEatenPost = 0;
}
// Input now contains bytesReadFromInput worth of floats, in the input sample rate
inpSize = bytesReadFromInput;
inpOffset = 0;
}
ioNumberPackets = (UInt32)(inpSize - inpOffset);
ioNumberPackets -= ioNumberPackets % floatFormat.mBytesPerPacket;
if(ioNumberPackets) {
size_t inputSamples = ioNumberPackets / floatFormat.mBytesPerPacket;
ioNumberPackets = (UInt32)inputSamples;
ioNumberPackets = (UInt32)ceil((float)ioNumberPackets * sampleRatio);
ioNumberPackets += soxr_delay(soxr);
ioNumberPackets = (ioNumberPackets + 255) & ~255;
size_t newSize = ioNumberPackets * floatFormat.mBytesPerPacket;
if(!floatBuffer || floatBufferSize < newSize) {
floatBuffer = realloc(floatBuffer, floatBufferSize = newSize * 3);
}
if(stopping) {
convertEntered = NO;
return nil;
}
size_t inputDone = 0;
size_t outputDone = 0;
if(!skipResampler) {
soxr_process(soxr, (float *)(((uint8_t *)inputBuffer) + inpOffset), inputSamples, &inputDone, floatBuffer, ioNumberPackets, &outputDone);
if(latencyEatenPost) {
// Post file or format change flush
size_t idone = 0, odone = 0;
do {
soxr_process(soxr, NULL, 0, &idone, floatBuffer + outputDone * floatFormat.mBytesPerPacket, ioNumberPackets - outputDone, &odone);
outputDone += odone;
} while(odone > 0);
}
} else {
memcpy(floatBuffer, (((uint8_t *)inputBuffer) + inpOffset), inputSamples * floatFormat.mBytesPerPacket);
inputDone = inputSamples;
outputDone = inputSamples;
}
inpOffset += inputDone * floatFormat.mBytesPerPacket;
if(latencyEaten) {
if(outputDone > latencyEaten) {
outputDone -= latencyEaten;
memmove(floatBuffer, floatBuffer + latencyEaten * floatFormat.mBytesPerPacket, outputDone * floatFormat.mBytesPerPacket);
latencyEaten = 0;
} else {
latencyEaten -= outputDone;
outputDone = 0;
}
}
if(latencyEatenPost) {
if(outputDone > latencyEatenPost) {
outputDone -= latencyEatenPost;
} else {
outputDone = 0;
}
latencyEatenPost = 0;
}
ioNumberPackets = (UInt32)outputDone * floatFormat.mBytesPerPacket;
}
if(ioNumberPackets) {
AudioChunk *chunk = [[AudioChunk alloc] init];
[chunk setFormat:nodeFormat];
if(nodeChannelConfig) {
[chunk setChannelConfig:nodeChannelConfig];
}
[self addObservers];
scale_by_volume(floatBuffer, ioNumberPackets / sizeof(float), volumeScale);
[chunk setStreamTimestamp:streamTimestamp];
[chunk setStreamTimeRatio:streamTimeRatio];
[chunk assignSamples:floatBuffer frameCount:ioNumberPackets / floatFormat.mBytesPerPacket];
streamTimestamp += [chunk durationRatioed];
convertEntered = NO;
return chunk;
}
convertEntered = NO;
return nil;
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if(context == kConverterNodeContext) {
DLog(@"SOMETHING CHANGED!");
if([keyPath isEqualToString:@"values.volumeScaling"]) {
// User reset the volume scaling option
[self refreshVolumeScaling];
}
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
static float db_to_scale(float db) {
return pow(10.0, db / 20);
}
- (void)refreshVolumeScaling {
if(rgInfo == nil) {
volumeScale = 1.0;
return;
}
NSString *scaling = [[NSUserDefaults standardUserDefaults] stringForKey:@"volumeScaling"];
BOOL useAlbum = [scaling hasPrefix:@"albumGain"];
BOOL useTrack = useAlbum || [scaling hasPrefix:@"trackGain"];
BOOL useVolume = useAlbum || useTrack || [scaling isEqualToString:@"volumeScale"];
BOOL usePeak = [scaling hasSuffix:@"WithPeak"];
float scale = 1.0;
float peak = 0.0;
if(useVolume) {
id pVolumeScale = [rgInfo objectForKey:@"volume"];
if(pVolumeScale != nil)
scale = [pVolumeScale floatValue];
}
if(useTrack) {
id trackGain = [rgInfo objectForKey:@"replayGainTrackGain"];
id trackPeak = [rgInfo objectForKey:@"replayGainTrackPeak"];
if(trackGain != nil)
scale = db_to_scale([trackGain floatValue]);
if(trackPeak != nil)
peak = [trackPeak floatValue];
}
if(useAlbum) {
id albumGain = [rgInfo objectForKey:@"replayGainAlbumGain"];
id albumPeak = [rgInfo objectForKey:@"replayGainAlbumPeak"];
if(albumGain != nil)
scale = db_to_scale([albumGain floatValue]);
if(albumPeak != nil)
peak = [albumPeak floatValue];
}
if(usePeak) {
if(scale * peak > 1.0)
scale = 1.0 / peak;
}
volumeScale = scale;
}
- (BOOL)setupWithInputFormat:(AudioStreamBasicDescription)inf withInputConfig:(uint32_t)inputConfig outputFormat:(AudioStreamBasicDescription)outf isLossless:(BOOL)lossless {
// Make the converter
inputFormat = inf;
outputFormat = outf;
inputChannelConfig = inputConfig;
rememberedLossless = lossless;
// These are the only sample formats we support translating
BOOL isFloat = !!(inputFormat.mFormatFlags & kAudioFormatFlagIsFloat);
if((!isFloat && !(inputFormat.mBitsPerChannel >= 1 && inputFormat.mBitsPerChannel <= 32)) || (isFloat && !(inputFormat.mBitsPerChannel == 32 || inputFormat.mBitsPerChannel == 64)))
return NO;
floatFormat = inputFormat;
floatFormat.mFormatFlags = kAudioFormatFlagsNativeFloatPacked;
floatFormat.mBitsPerChannel = 32;
floatFormat.mBytesPerFrame = (32 / 8) * floatFormat.mChannelsPerFrame;
floatFormat.mBytesPerPacket = floatFormat.mBytesPerFrame * floatFormat.mFramesPerPacket;
#if DSD_DECIMATE
if(inputFormat.mBitsPerChannel == 1) {
// Decimate this for speed
floatFormat.mSampleRate *= 1.0 / 8.0;
}
#endif
inpOffset = 0;
inpSize = 0;
// This is a post resampler format
nodeFormat = floatFormat;
nodeFormat.mSampleRate = outputFormat.mSampleRate;
nodeChannelConfig = inputChannelConfig;
sampleRatio = (double)outputFormat.mSampleRate / (double)floatFormat.mSampleRate;
skipResampler = fabs(sampleRatio - 1.0) < 1e-7;
if(!skipResampler) {
soxr_quality_spec_t q_spec = soxr_quality_spec(SOXR_HQ, 0);
soxr_io_spec_t io_spec = soxr_io_spec(SOXR_FLOAT32_I, SOXR_FLOAT32_I);
soxr_runtime_spec_t runtime_spec = soxr_runtime_spec(0);
soxr_error_t error;
soxr = soxr_create(floatFormat.mSampleRate, outputFormat.mSampleRate, floatFormat.mChannelsPerFrame, &error, &io_spec, &q_spec, &runtime_spec);
if(error)
return NO;
PRIME_LEN_ = MAX(floatFormat.mSampleRate / 20, 1024u);
PRIME_LEN_ = MIN(PRIME_LEN_, 16384u);
PRIME_LEN_ = MAX(PRIME_LEN_, (unsigned int)(2 * LPC_ORDER + 1));
N_samples_to_add_ = floatFormat.mSampleRate;
N_samples_to_drop_ = outputFormat.mSampleRate;
samples_len(&N_samples_to_add_, &N_samples_to_drop_, 20, 8192u);
is_preextrapolated_ = NO;
is_postextrapolated_ = 0;
}
latencyEaten = 0;
latencyEatenPost = 0;
PrintStreamDesc(&inf);
PrintStreamDesc(&nodeFormat);
[self refreshVolumeScaling];
// Move this here so process call isn't running the resampler until it's allocated
stopping = NO;
convertEntered = NO;
streamFormatChanged = NO;
paused = NO;
return YES;
}
- (void)dealloc {
DLog(@"Converter dealloc");
[self removeObservers];
paused = NO;
[self cleanUp];
[super cleanUp];
}
- (void)setOutputFormat:(AudioStreamBasicDescription)format {
DLog(@"SETTING OUTPUT FORMAT!");
outputFormat = format;
}
- (void)inputFormatDidChange:(AudioStreamBasicDescription)format inputConfig:(uint32_t)inputConfig {
DLog(@"FORMAT CHANGED");
paused = YES;
while(convertEntered) {
usleep(500);
}
[self cleanUp];
[self setupWithInputFormat:format withInputConfig:inputConfig outputFormat:self->outputFormat isLossless:rememberedLossless];
}
- (void)setRGInfo:(NSDictionary *)rgi {
DLog(@"Setting ReplayGain info");
rgInfo = rgi;
[self refreshVolumeScaling];
}
- (void)cleanUp {
stopping = YES;
while(convertEntered) {
usleep(500);
}
if(soxr) {
soxr_delete(soxr);
soxr = NULL;
}
if(extrapolateBuffer) {
free(extrapolateBuffer);
extrapolateBuffer = NULL;
extrapolateBufferSize = 0;
}
if(floatBuffer) {
free(floatBuffer);
floatBuffer = NULL;
floatBufferSize = 0;
}
if(inputBuffer) {
free(inputBuffer);
inputBuffer = NULL;
inputBufferSize = 0;
}
inpOffset = 0;
inpSize = 0;
}
- (double)secondsBuffered {
return [buffer listDuration];
}
@end

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,34 @@
//
// DSPDownmixNode.h
// CogAudio
//
// Created by Christopher Snowhill on 2/13/25.
//
#ifndef DSPDownmixNode_h
#define DSPDownmixNode_h
#import <AudioToolbox/AudioToolbox.h>
#import <CogAudio/DSPNode.h>
@interface DSPDownmixNode : DSPNode {
}
- (id _Nullable)initWithController:(id _Nonnull)c previous:(id _Nullable)p latency:(double)latency;
- (BOOL)setup;
- (void)cleanUp;
- (void)resetBuffer;
- (BOOL)paused;
- (void)process;
- (AudioChunk * _Nullable)convert;
- (void)setOutputFormat:(AudioStreamBasicDescription)format withChannelConfig:(uint32_t)config;
@end
#endif /* DSPDownmixNode_h */

View file

@ -0,0 +1,201 @@
//
// DSPDownmixNode.m
// CogAudio Framework
//
// Created by Christopher Snowhill on 2/13/25.
//
#import <Foundation/Foundation.h>
#import "Downmix.h"
#import "Logging.h"
#import "DSPDownmixNode.h"
@implementation DSPDownmixNode {
DownmixProcessor *downmix;
BOOL stopping, paused;
BOOL processEntered;
BOOL formatSet;
AudioStreamBasicDescription lastInputFormat;
AudioStreamBasicDescription inputFormat;
AudioStreamBasicDescription outputFormat;
uint32_t lastInputChannelConfig, inputChannelConfig;
uint32_t outputChannelConfig;
float outBuffer[4096 * 32];
}
- (id _Nullable)initWithController:(id _Nonnull)c previous:(id _Nullable)p latency:(double)latency {
self = [super initWithController:c previous:p latency:latency];
return self;
}
- (void)dealloc {
DLog(@"Downmix dealloc");
[self setShouldContinue:NO];
[self cleanUp];
[super cleanUp];
}
- (BOOL)fullInit {
if(formatSet) {
downmix = [[DownmixProcessor alloc] initWithInputFormat:inputFormat inputConfig:inputChannelConfig andOutputFormat:outputFormat outputConfig:outputChannelConfig];
if(!downmix) {
return NO;
}
}
return YES;
}
- (void)fullShutdown {
downmix = nil;
}
- (BOOL)setup {
if(stopping)
return NO;
[self fullShutdown];
return [self fullInit];
}
- (void)cleanUp {
stopping = YES;
while(processEntered) {
usleep(500);
}
[self fullShutdown];
formatSet = NO;
}
- (void)resetBuffer {
paused = YES;
while(processEntered) {
usleep(500);
}
[buffer reset];
paused = NO;
}
- (void)setOutputFormat:(AudioStreamBasicDescription)format withChannelConfig:(uint32_t)config {
if(memcmp(&outputFormat, &format, sizeof(outputFormat)) != 0 ||
outputChannelConfig != config) {
paused = YES;
while(processEntered) {
usleep(500);
}
[buffer reset];
[self fullShutdown];
paused = NO;
}
outputFormat = format;
outputChannelConfig = config;
formatSet = YES;
}
- (BOOL)paused {
return paused;
}
- (void)process {
while([self shouldContinue] == YES) {
if(paused || endOfStream) {
usleep(500);
continue;
}
@autoreleasepool {
AudioChunk *chunk = nil;
chunk = [self convert];
if(!chunk || ![chunk frameCount]) {
if([previousNode endOfStream] == YES) {
usleep(500);
endOfStream = YES;
continue;
}
if(paused) {
continue;
}
usleep(500);
} else {
[self writeChunk:chunk];
chunk = nil;
}
}
}
}
- (AudioChunk *)convert {
if(stopping)
return nil;
processEntered = YES;
if(stopping || ([[previousNode buffer] isEmpty] && [previousNode endOfStream] == YES) || [self shouldContinue] == NO) {
processEntered = NO;
return nil;
}
if(![self peekFormat:&inputFormat channelConfig:&inputChannelConfig]) {
processEntered = NO;
return nil;
}
if(!inputFormat.mSampleRate ||
!inputFormat.mBitsPerChannel ||
!inputFormat.mChannelsPerFrame ||
!inputFormat.mBytesPerFrame ||
!inputFormat.mFramesPerPacket ||
!inputFormat.mBytesPerPacket) {
processEntered = NO;
return nil;
}
if((formatSet && !downmix) ||
memcmp(&inputFormat, &lastInputFormat, sizeof(inputFormat)) != 0 ||
inputChannelConfig != lastInputChannelConfig) {
lastInputFormat = inputFormat;
lastInputChannelConfig = inputChannelConfig;
[self fullShutdown];
if(formatSet && ![self setup]) {
processEntered = NO;
return nil;
}
}
if(!downmix) {
processEntered = NO;
return [self readChunk:4096];
}
AudioChunk *chunk = [self readChunkAsFloat32:4096];
if(!chunk || ![chunk frameCount]) {
processEntered = NO;
return nil;
}
double streamTimestamp = [chunk streamTimestamp];
size_t frameCount = [chunk frameCount];
NSData *sampleData = [chunk removeSamples:frameCount];
[downmix process:[sampleData bytes] frameCount:frameCount output:&outBuffer[0]];
AudioChunk *outputChunk = [[AudioChunk alloc] init];
[outputChunk setFormat:outputFormat];
if(outputChannelConfig) {
[outputChunk setChannelConfig:outputChannelConfig];
}
if([chunk isHDCD]) [outputChunk setHDCD];
[outputChunk setStreamTimestamp:streamTimestamp];
[outputChunk setStreamTimeRatio:[chunk streamTimeRatio]];
[outputChunk assignSamples:&outBuffer[0] frameCount:frameCount];
processEntered = NO;
return outputChunk;
}
@end

View file

@ -0,0 +1,31 @@
//
// DSPEqualizerNode.h
// CogAudio
//
// Created by Christopher Snowhill on 2/11/25.
//
#ifndef DSPEqualizerNode_h
#define DSPEqualizerNode_h
#import <CogAudio/DSPNode.h>
@interface DSPEqualizerNode : DSPNode {
float *samplePtr;
}
- (id _Nullable)initWithController:(id _Nonnull)c previous:(id _Nullable)p latency:(double)latency;
- (BOOL)setup;
- (void)cleanUp;
- (void)resetBuffer;
- (BOOL)paused;
- (void)process;
- (AudioChunk * _Nullable)convert;
@end
#endif /* DSPEqualizerNode_h */

View file

@ -0,0 +1,401 @@
//
// DSPEqualizerNode.m
// CogAudio Framework
//
// Created by Christopher Snowhill on 2/11/25.
//
#import <Foundation/Foundation.h>
#import <AudioToolbox/AudioToolbox.h>
#import <AudioUnit/AudioUnit.h>
#import <Accelerate/Accelerate.h>
#import "DSPEqualizerNode.h"
#import "OutputNode.h"
#import "Logging.h"
#import "AudioPlayer.h"
extern void scale_by_volume(float *buffer, size_t count, float volume);
static void * kDSPEqualizerNodeContext = &kDSPEqualizerNodeContext;
@implementation DSPEqualizerNode {
BOOL enableEqualizer;
BOOL equalizerInitialized;
double equalizerPreamp;
__weak AudioPlayer *audioPlayer;
AudioUnit _eq;
AudioTimeStamp timeStamp;
BOOL stopping, paused;
BOOL processEntered;
BOOL observersapplied;
AudioStreamBasicDescription lastInputFormat;
AudioStreamBasicDescription inputFormat;
uint32_t lastInputChannelConfig, inputChannelConfig;
uint32_t outputChannelConfig;
float inBuffer[4096 * 32];
float eqBuffer[4096 * 32];
float outBuffer[4096 * 32];
}
static void fillBuffers(AudioBufferList *ioData, const float *inbuffer, size_t count, size_t offset) {
const size_t channels = ioData->mNumberBuffers;
for(int i = 0; i < channels; ++i) {
const size_t maxCount = (ioData->mBuffers[i].mDataByteSize / sizeof(float)) - offset;
float *output = ((float *)ioData->mBuffers[i].mData) + offset;
const float *input = inbuffer + i;
cblas_scopy((int)((count > maxCount) ? maxCount : count), input, (int)channels, output, 1);
ioData->mBuffers[i].mNumberChannels = 1;
}
}
static void clearBuffers(AudioBufferList *ioData, size_t count, size_t offset) {
for(int i = 0; i < ioData->mNumberBuffers; ++i) {
memset((uint8_t *)ioData->mBuffers[i].mData + offset * sizeof(float), 0, count * sizeof(float));
ioData->mBuffers[i].mNumberChannels = 1;
}
}
static OSStatus eqRenderCallback(void *inRefCon, AudioUnitRenderActionFlags *ioActionFlags, const AudioTimeStamp *inTimeStamp, UInt32 inBusNumber, UInt32 inNumberFrames, AudioBufferList *ioData) {
if(inNumberFrames > 4096 || !inRefCon) {
clearBuffers(ioData, inNumberFrames, 0);
return 0;
}
DSPEqualizerNode *_self = (__bridge DSPEqualizerNode *)inRefCon;
fillBuffers(ioData, _self->samplePtr, inNumberFrames, 0);
return 0;
}
- (id _Nullable)initWithController:(id _Nonnull)c previous:(id _Nullable)p latency:(double)latency {
self = [super initWithController:c previous:p latency:latency];
if(self) {
NSUserDefaults *defaults = [[NSUserDefaultsController sharedUserDefaultsController] defaults];
enableEqualizer = [defaults boolForKey:@"GraphicEQenable"];
float preamp = [defaults floatForKey:@"eqPreamp"];
equalizerPreamp = pow(10.0, preamp / 20.0);
OutputNode *outputNode = c;
audioPlayer = [outputNode controller];
[self addObservers];
}
return self;
}
- (void)dealloc {
DLog(@"Equalizer dealloc");
[self setShouldContinue:NO];
[self cleanUp];
[self removeObservers];
[super cleanUp];
}
- (void)addObservers {
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.GraphicEQenable" options:0 context:kDSPEqualizerNodeContext];
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.eqPreamp" options:0 context:kDSPEqualizerNodeContext];
observersapplied = YES;
}
- (void)removeObservers {
if(observersapplied) {
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.GraphicEQenable" context:kDSPEqualizerNodeContext];
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.eqPreamp" context:kDSPEqualizerNodeContext];
observersapplied = NO;
}
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if(context != kDSPEqualizerNodeContext) {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
return;
}
if([keyPath isEqualToString:@"values.GraphicEQenable"]) {
NSUserDefaults *defaults = [[NSUserDefaultsController sharedUserDefaultsController] defaults];
enableEqualizer = [defaults boolForKey:@"GraphicEQenable"];
} else if([keyPath isEqualToString:@"values.eqPreamp"]) {
NSUserDefaults *defaults = [[NSUserDefaultsController sharedUserDefaultsController] defaults];
float preamp = [defaults floatForKey:@"eqPreamp"];
equalizerPreamp = pow(10.0, preamp / 20.0);
}
}
- (AudioPlayer *)audioPlayer {
return audioPlayer;
}
- (BOOL)fullInit {
if(enableEqualizer) {
AudioComponentDescription desc;
NSError *err;
desc.componentType = kAudioUnitType_Effect;
desc.componentSubType = kAudioUnitSubType_GraphicEQ;
desc.componentManufacturer = kAudioUnitManufacturer_Apple;
desc.componentFlags = 0;
desc.componentFlagsMask = 0;
AudioComponent comp = NULL;
comp = AudioComponentFindNext(comp, &desc);
if(!comp) {
return NO;
}
OSStatus _err = AudioComponentInstanceNew(comp, &_eq);
if(err) {
return NO;
}
UInt32 value;
UInt32 size = sizeof(value);
value = 4096;
AudioUnitSetProperty(_eq, kAudioUnitProperty_MaximumFramesPerSlice,
kAudioUnitScope_Global, 0, &value, size);
value = 127;
AudioUnitSetProperty(_eq, kAudioUnitProperty_RenderQuality,
kAudioUnitScope_Global, 0, &value, size);
AURenderCallbackStruct callbackStruct;
callbackStruct.inputProcRefCon = (__bridge void *)self;
callbackStruct.inputProc = eqRenderCallback;
AudioUnitSetProperty(_eq, kAudioUnitProperty_SetRenderCallback,
kAudioUnitScope_Input, 0, &callbackStruct, sizeof(callbackStruct));
AudioUnitReset(_eq, kAudioUnitScope_Input, 0);
AudioUnitReset(_eq, kAudioUnitScope_Output, 0);
AudioUnitReset(_eq, kAudioUnitScope_Global, 0);
AudioStreamBasicDescription asbd = inputFormat;
// Of course, non-interleaved has only one sample per frame/packet, per buffer
asbd.mFormatFlags |= kAudioFormatFlagIsNonInterleaved;
asbd.mBytesPerFrame = sizeof(float);
asbd.mBytesPerPacket = sizeof(float);
asbd.mFramesPerPacket = 1;
UInt32 maximumFrames = 4096;
AudioUnitSetProperty(_eq, kAudioUnitProperty_MaximumFramesPerSlice, kAudioUnitScope_Global, 0, &maximumFrames, sizeof(maximumFrames));
AudioUnitSetProperty(_eq, kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Input, 0, &asbd, sizeof(asbd));
AudioUnitSetProperty(_eq, kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Output, 0, &asbd, sizeof(asbd));
AudioUnitReset(_eq, kAudioUnitScope_Input, 0);
AudioUnitReset(_eq, kAudioUnitScope_Output, 0);
AudioUnitReset(_eq, kAudioUnitScope_Global, 0);
_err = AudioUnitInitialize(_eq);
if(_err != noErr) {
return NO;
}
bzero(&timeStamp, sizeof(timeStamp));
timeStamp.mFlags = kAudioTimeStampSampleTimeValid;
equalizerInitialized = YES;
[[self audioPlayer] beginEqualizer:_eq];
}
return YES;
}
- (void)fullShutdown {
if(_eq) {
if(equalizerInitialized) {
[[self audioPlayer] endEqualizer:_eq];
AudioUnitUninitialize(_eq);
equalizerInitialized = NO;
}
AudioComponentInstanceDispose(_eq);
_eq = NULL;
}
}
- (BOOL)setup {
if(stopping)
return NO;
[self fullShutdown];
return [self fullInit];
}
- (void)cleanUp {
stopping = YES;
while(processEntered) {
usleep(500);
}
[self fullShutdown];
}
- (void)resetBuffer {
paused = YES;
while(processEntered) {
usleep(500);
}
[buffer reset];
[self fullShutdown];
paused = NO;
}
- (BOOL)paused {
return paused;
}
- (void)process {
while([self shouldContinue] == YES) {
if(paused || endOfStream) {
usleep(500);
continue;
}
@autoreleasepool {
AudioChunk *chunk = nil;
chunk = [self convert];
if(!chunk || ![chunk frameCount]) {
if([previousNode endOfStream] == YES) {
usleep(500);
endOfStream = YES;
continue;
}
if(paused) {
continue;
}
usleep(500);
} else {
[self writeChunk:chunk];
chunk = nil;
}
if(!enableEqualizer && equalizerInitialized) {
[self fullShutdown];
}
}
}
}
- (AudioChunk *)convert {
if(stopping)
return nil;
processEntered = YES;
if(stopping || ([[previousNode buffer] isEmpty] && [previousNode endOfStream] == YES) || [self shouldContinue] == NO) {
processEntered = NO;
return nil;
}
if(![self peekFormat:&inputFormat channelConfig:&inputChannelConfig]) {
processEntered = NO;
return nil;
}
if(!inputFormat.mSampleRate ||
!inputFormat.mBitsPerChannel ||
!inputFormat.mChannelsPerFrame ||
!inputFormat.mBytesPerFrame ||
!inputFormat.mFramesPerPacket ||
!inputFormat.mBytesPerPacket) {
processEntered = NO;
return nil;
}
if((enableEqualizer && !equalizerInitialized) ||
memcmp(&inputFormat, &lastInputFormat, sizeof(inputFormat)) != 0 ||
inputChannelConfig != lastInputChannelConfig) {
lastInputFormat = inputFormat;
lastInputChannelConfig = inputChannelConfig;
[self fullShutdown];
if(enableEqualizer && ![self setup]) {
processEntered = NO;
return nil;
}
}
if(!equalizerInitialized) {
processEntered = NO;
return [self readChunk:4096];
}
AudioChunk *chunk = [self readChunkAsFloat32:4096];
if(!chunk || ![chunk frameCount]) {
processEntered = NO;
return nil;
}
double streamTimestamp = [chunk streamTimestamp];
samplePtr = &inBuffer[0];
size_t channels = inputFormat.mChannelsPerFrame;
size_t frameCount = [chunk frameCount];
NSData *sampleData = [chunk removeSamples:frameCount];
cblas_scopy((int)(frameCount * channels), [sampleData bytes], 1, &inBuffer[0], 1);
const size_t channelsminusone = channels - 1;
uint8_t tempBuffer[sizeof(AudioBufferList) + sizeof(AudioBuffer) * channelsminusone];
AudioBufferList *ioData = (AudioBufferList *)&tempBuffer[0];
ioData->mNumberBuffers = (UInt32)channels;
for(size_t i = 0; i < channels; ++i) {
ioData->mBuffers[i].mData = &eqBuffer[4096 * i];
ioData->mBuffers[i].mDataByteSize = (UInt32)(frameCount * sizeof(float));
ioData->mBuffers[i].mNumberChannels = 1;
}
OSStatus status = AudioUnitRender(_eq, NULL, &timeStamp, 0, (UInt32)frameCount, ioData);
if(status != noErr) {
processEntered = NO;
return nil;
}
timeStamp.mSampleTime += ((double)frameCount) / inputFormat.mSampleRate;
for(int i = 0; i < channels; ++i) {
cblas_scopy((int)frameCount, &eqBuffer[4096 * i], 1, &outBuffer[i], (int)channels);
}
AudioChunk *outputChunk = nil;
if(frameCount) {
scale_by_volume(&outBuffer[0], frameCount * channels, equalizerPreamp);
outputChunk = [[AudioChunk alloc] init];
[outputChunk setFormat:inputFormat];
if(outputChannelConfig) {
[outputChunk setChannelConfig:inputChannelConfig];
}
if([chunk isHDCD]) [outputChunk setHDCD];
[outputChunk setStreamTimestamp:streamTimestamp];
[outputChunk setStreamTimeRatio:[chunk streamTimeRatio]];
[outputChunk assignSamples:&outBuffer[0] frameCount:frameCount];
}
processEntered = NO;
return outputChunk;
}
@end

View file

@ -0,0 +1,30 @@
//
// DSPFSurroundNode.h
// CogAudio
//
// Created by Christopher Snowhill on 2/11/25.
//
#ifndef DSPFSurroundNode_h
#define DSPFSurroundNode_h
#import <CogAudio/DSPNode.h>
@interface DSPFSurroundNode : DSPNode {
}
- (id _Nullable)initWithController:(id _Nonnull)c previous:(id _Nullable)p latency:(double)latency;
- (BOOL)setup;
- (void)cleanUp;
- (void)resetBuffer;
- (BOOL)paused;
- (void)process;
- (AudioChunk * _Nullable)convert;
@end
#endif /* DSPFSurroundNode_h */

View file

@ -0,0 +1,275 @@
//
// DSPFSurroundNode.m
// CogAudio Framework
//
// Created by Christopher Snowhill on 2/11/25.
//
#import <Foundation/Foundation.h>
#import <Accelerate/Accelerate.h>
#import "DSPFSurroundNode.h"
#import "Logging.h"
#import "FSurroundFilter.h"
#define OCTAVES 5
static void * kDSPFSurroundNodeContext = &kDSPFSurroundNodeContext;
@implementation DSPFSurroundNode {
BOOL enableFSurround;
BOOL FSurroundDelayRemoved;
FSurroundFilter *fsurround;
BOOL stopping, paused;
BOOL processEntered;
BOOL observersapplied;
AudioStreamBasicDescription lastInputFormat;
AudioStreamBasicDescription inputFormat;
AudioStreamBasicDescription outputFormat;
uint32_t lastInputChannelConfig, inputChannelConfig;
uint32_t outputChannelConfig;
float inBuffer[4096 * 2];
float outBuffer[8192 * 6];
}
- (id _Nullable)initWithController:(id _Nonnull)c previous:(id _Nullable)p latency:(double)latency {
self = [super initWithController:c previous:p latency:latency];
if(self) {
NSUserDefaults *defaults = [[NSUserDefaultsController sharedUserDefaultsController] defaults];
enableFSurround = [defaults boolForKey:@"enableFSurround"];
[self addObservers];
}
return self;
}
- (void)dealloc {
DLog(@"FreeSurround dealloc");
[self setShouldContinue:NO];
[self cleanUp];
[self removeObservers];
[super cleanUp];
}
- (void)addObservers {
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.enableFSurround" options:(NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew) context:kDSPFSurroundNodeContext];
observersapplied = YES;
}
- (void)removeObservers {
if(observersapplied) {
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.enableFSurround" context:kDSPFSurroundNodeContext];
observersapplied = NO;
}
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if(context != kDSPFSurroundNodeContext) {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
return;
}
if([keyPath isEqualToString:@"values.enableFSurround"]) {
NSUserDefaults *defaults = [[NSUserDefaultsController sharedUserDefaultsController] defaults];
enableFSurround = [defaults boolForKey:@"enableFSurround"];
}
}
- (BOOL)fullInit {
if(enableFSurround && inputFormat.mChannelsPerFrame == 2) {
fsurround = [[FSurroundFilter alloc] initWithSampleRate:inputFormat.mSampleRate];
if(!fsurround) {
return NO;
}
outputFormat = inputFormat;
outputFormat.mChannelsPerFrame = [fsurround channelCount];
outputFormat.mBytesPerFrame = sizeof(float) * outputFormat.mChannelsPerFrame;
outputFormat.mBytesPerPacket = outputFormat.mBytesPerFrame * outputFormat.mFramesPerPacket;
outputChannelConfig = [fsurround channelConfig];
FSurroundDelayRemoved = NO;
} else {
fsurround = nil;
}
return YES;
}
- (void)fullShutdown {
fsurround = nil;
}
- (BOOL)setup {
if(stopping)
return NO;
[self fullShutdown];
return [self fullInit];
}
- (void)cleanUp {
stopping = YES;
while(processEntered) {
usleep(500);
}
[self fullShutdown];
}
- (void)resetBuffer {
paused = YES;
while(processEntered) {
usleep(500);
}
[buffer reset];
[self fullShutdown];
paused = NO;
}
- (BOOL)paused {
return paused;
}
- (void)process {
while([self shouldContinue] == YES) {
if(paused || endOfStream) {
usleep(500);
continue;
}
@autoreleasepool {
AudioChunk *chunk = nil;
chunk = [self convert];
if(!chunk || ![chunk frameCount]) {
if([previousNode endOfStream] == YES) {
usleep(500);
endOfStream = YES;
continue;
}
if(paused) {
continue;
}
usleep(500);
} else {
[self writeChunk:chunk];
chunk = nil;
}
if(!enableFSurround && fsurround) {
[self fullShutdown];
}
}
}
}
- (AudioChunk *)convert {
if(stopping)
return nil;
processEntered = YES;
if(stopping || ([[previousNode buffer] isEmpty] && [previousNode endOfStream] == YES) || [self shouldContinue] == NO) {
processEntered = NO;
return nil;
}
if(![self peekFormat:&inputFormat channelConfig:&inputChannelConfig]) {
processEntered = NO;
return nil;
}
if(!inputFormat.mSampleRate ||
!inputFormat.mBitsPerChannel ||
!inputFormat.mChannelsPerFrame ||
!inputFormat.mBytesPerFrame ||
!inputFormat.mFramesPerPacket ||
!inputFormat.mBytesPerPacket) {
processEntered = NO;
return nil;
}
if((enableFSurround && !fsurround) ||
memcmp(&inputFormat, &lastInputFormat, sizeof(inputFormat)) != 0 ||
inputChannelConfig != lastInputChannelConfig) {
lastInputFormat = inputFormat;
lastInputChannelConfig = inputChannelConfig;
[self fullShutdown];
if(enableFSurround && ![self setup]) {
processEntered = NO;
return nil;
}
}
if(!fsurround) {
processEntered = NO;
return [self readChunk:4096];
}
size_t totalRequestedSamples = 4096;
size_t totalFrameCount = 0;
AudioChunk *chunk = [self readAndMergeChunksAsFloat32:totalRequestedSamples];
if(!chunk || ![chunk frameCount]) {
processEntered = NO;
return nil;
}
double streamTimestamp = [chunk streamTimestamp];
float *samplePtr = &inBuffer[0];
size_t frameCount = [chunk frameCount];
NSData *sampleData = [chunk removeSamples:frameCount];
cblas_scopy((int)frameCount * 2, [sampleData bytes], 1, &samplePtr[0], 1);
totalFrameCount = frameCount;
size_t countToProcess = totalFrameCount;
size_t samplesRendered;
if(countToProcess < 4096) {
bzero(&inBuffer[countToProcess * 2], (4096 - countToProcess) * 2 * sizeof(float));
countToProcess = 4096;
}
[fsurround process:&inBuffer[0] output:&outBuffer[0] count:(int)countToProcess];
samplePtr = &outBuffer[0];
samplesRendered = totalFrameCount;
if(totalFrameCount < 4096) {
bzero(&outBuffer[4096 * 6], 4096 * 2 * sizeof(float));
[fsurround process:&outBuffer[4096 * 6] output:&outBuffer[4096 * 6] count:4096];
samplesRendered += 2048;
}
if(!FSurroundDelayRemoved) {
FSurroundDelayRemoved = YES;
if(samplesRendered > 2048) {
samplePtr += 2048 * 6;
samplesRendered -= 2048;
}
}
AudioChunk *outputChunk = nil;
if(samplesRendered) {
outputChunk = [[AudioChunk alloc] init];
[outputChunk setFormat:outputFormat];
if(outputChannelConfig) {
[outputChunk setChannelConfig:outputChannelConfig];
}
if([chunk isHDCD]) [outputChunk setHDCD];
[outputChunk setStreamTimestamp:streamTimestamp];
[outputChunk setStreamTimeRatio:[chunk streamTimeRatio]];
[outputChunk assignSamples:samplePtr frameCount:samplesRendered];
}
processEntered = NO;
return outputChunk;
}
@end

View file

@ -0,0 +1,35 @@
//
// DSPHRTFNode.h
// CogAudio
//
// Created by Christopher Snowhill on 2/11/25.
//
#ifndef DSPHRTFNode_h
#define DSPHRTFNode_h
#import <simd/types.h>
#import <CogAudio/DSPNode.h>
@interface DSPHRTFNode : DSPNode {
}
- (id _Nullable)initWithController:(id _Nonnull)c previous:(id _Nullable)p latency:(double)latency;
- (BOOL)setup;
- (void)cleanUp;
- (void)resetBuffer;
- (BOOL)paused;
- (void)process;
- (AudioChunk * _Nullable)convert;
- (void)reportMotion:(simd_float4x4)matrix;
- (void)resetReferencePosition:(NSNotification *_Nullable)notification;
@end
#endif /* DSPHRTFNode_h */

View file

@ -0,0 +1,434 @@
//
// DSPHRTFNode.m
// CogAudio Framework
//
// Created by Christopher Snowhill on 2/11/25.
//
#import <Foundation/Foundation.h>
#import <CoreMotion/CoreMotion.h>
#import "Logging.h"
#import "DSPHRTFNode.h"
#import "lpc.h"
#import "HeadphoneFilter.h"
#include <AvailabilityMacros.h>
#if defined(MAC_OS_X_VERSION_14_0) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_14_0
#define MOTION_MANAGER 1
#endif
static void * kDSPHRTFNodeContext = &kDSPHRTFNodeContext;
static NSString *CogPlaybackDidResetHeadTracking = @"CogPlaybackDigResetHeadTracking";
static simd_float4x4 convertMatrix(CMRotationMatrix r) {
simd_float4x4 matrix = {
simd_make_float4(r.m33, -r.m31, r.m32, 0.0f),
simd_make_float4(r.m13, -r.m11, r.m12, 0.0f),
simd_make_float4(r.m23, -r.m21, r.m22, 0.0f),
simd_make_float4(0.0f, 0.0f, 0.0f, 1.0f)
};
return matrix;
}
#ifdef MOTION_MANAGER
static NSLock *motionManagerLock = nil;
API_AVAILABLE(macos(14.0)) static CMHeadphoneMotionManager *motionManager = nil;
static DSPHRTFNode *registeredMotionListener = nil;
#endif
static void registerMotionListener(DSPHRTFNode *listener) {
#ifdef MOTION_MANAGER
if(@available(macOS 14, *)) {
[motionManagerLock lock];
if([motionManager isDeviceMotionActive]) {
[motionManager stopDeviceMotionUpdates];
}
if([motionManager isDeviceMotionAvailable]) {
registeredMotionListener = listener;
[motionManager startDeviceMotionUpdatesToQueue:[NSOperationQueue mainQueue] withHandler:^(CMDeviceMotion * _Nullable motion, NSError * _Nullable error) {
if(motion) {
[motionManagerLock lock];
[registeredMotionListener reportMotion:convertMatrix(motion.attitude.rotationMatrix)];
[motionManagerLock unlock];
}
}];
}
[motionManagerLock unlock];
}
#endif
}
static void unregisterMotionListener(void) {
#ifdef MOTION_MANAGER
if(@available(macOS 14, *)) {
[motionManagerLock lock];
if([motionManager isDeviceMotionActive]) {
[motionManager stopDeviceMotionUpdates];
}
registeredMotionListener = nil;
[motionManagerLock unlock];
}
#endif
}
@implementation DSPHRTFNode {
BOOL enableHrtf;
BOOL enableHeadTracking;
BOOL lastEnableHeadTracking;
HeadphoneFilter *hrtf;
BOOL stopping, paused;
BOOL processEntered;
BOOL resetFilter;
size_t needPrefill;
BOOL observersapplied;
AudioStreamBasicDescription lastInputFormat;
AudioStreamBasicDescription inputFormat;
AudioStreamBasicDescription outputFormat;
uint32_t lastInputChannelConfig, inputChannelConfig;
uint32_t outputChannelConfig;
BOOL referenceMatrixSet;
BOOL rotationMatrixUpdated;
simd_float4x4 rotationMatrix;
simd_float4x4 referenceMatrix;
float prefillBuffer[4096 * 32];
float outBuffer[4096 * 2];
void *extrapolate_buffer;
size_t extrapolate_buffer_size;
}
+ (void)initialize {
#ifdef MOTION_MANAGER
motionManagerLock = [[NSLock alloc] init];
if(@available(macOS 14, *)) {
CMAuthorizationStatus status = [CMHeadphoneMotionManager authorizationStatus];
if(status == CMAuthorizationStatusDenied) {
ALog(@"Headphone motion not authorized");
return;
} else if(status == CMAuthorizationStatusAuthorized) {
ALog(@"Headphone motion authorized");
} else if(status == CMAuthorizationStatusRestricted) {
ALog(@"Headphone motion restricted");
} else if(status == CMAuthorizationStatusNotDetermined) {
ALog(@"Headphone motion status not determined; will prompt for access");
}
motionManager = [[CMHeadphoneMotionManager alloc] init];
}
#endif
}
- (id _Nullable)initWithController:(id _Nonnull)c previous:(id _Nullable)p latency:(double)latency {
self = [super initWithController:c previous:p latency:latency];
if(self) {
NSUserDefaults *defaults = [[NSUserDefaultsController sharedUserDefaultsController] defaults];
enableHrtf = [defaults boolForKey:@"enableHrtf"];
enableHeadTracking = [defaults boolForKey:@"enableHeadTracking"];
rotationMatrix = matrix_identity_float4x4;
[self addObservers];
}
return self;
}
- (void)dealloc {
DLog(@"HRTF dealloc");
[self setShouldContinue:NO];
[self cleanUp];
[self removeObservers];
[super cleanUp];
if(extrapolate_buffer) {
free(extrapolate_buffer);
extrapolate_buffer = NULL;
extrapolate_buffer_size = 0;
}
}
- (void)addObservers {
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.enableHrtf" options:(NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew) context:kDSPHRTFNodeContext];
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.enableHeadTracking" options:(NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew) context:kDSPHRTFNodeContext];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(resetReferencePosition:) name:CogPlaybackDidResetHeadTracking object:nil];
observersapplied = YES;
}
- (void)removeObservers {
if(observersapplied) {
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.enableHrtf" context:kDSPHRTFNodeContext];
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.enableHeadTracking" context:kDSPHRTFNodeContext];
[[NSNotificationCenter defaultCenter] removeObserver:self name:CogPlaybackDidResetHeadTracking object:nil];
observersapplied = NO;
}
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if(context != kDSPHRTFNodeContext) {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
return;
}
if([keyPath isEqualToString:@"values.enableHrtf"] ||
[keyPath isEqualToString:@"values.enableHeadTracking"]) {
NSUserDefaults *defaults = [[NSUserDefaultsController sharedUserDefaultsController] defaults];
enableHrtf = [defaults boolForKey:@"enableHrtf"];
enableHeadTracking = [defaults boolForKey:@"enableHeadTracking"];
resetFilter = YES;
}
}
- (BOOL)fullInit {
if(enableHrtf) {
NSURL *presetUrl = [[NSBundle mainBundle] URLForResource:@"SADIE_D02-96000" withExtension:@"mhr"];
rotationMatrixUpdated = NO;
simd_float4x4 matrix;
if(!referenceMatrixSet || !enableHeadTracking) {
referenceMatrixSet = NO;
matrix = matrix_identity_float4x4;
self->referenceMatrix = matrix;
if(enableHeadTracking) {
lastEnableHeadTracking = YES;
registerMotionListener(self);
} else if(lastEnableHeadTracking) {
lastEnableHeadTracking = NO;
unregisterMotionListener();
}
} else {
simd_float4x4 mirrorTransform = {
simd_make_float4(-1.0, 0.0, 0.0, 0.0),
simd_make_float4(0.0, 1.0, 0.0, 0.0),
simd_make_float4(0.0, 0.0, 1.0, 0.0),
simd_make_float4(0.0, 0.0, 0.0, 1.0)
};
matrix = simd_mul(mirrorTransform, rotationMatrix);
matrix = simd_mul(matrix, referenceMatrix);
}
hrtf = [[HeadphoneFilter alloc] initWithImpulseFile:presetUrl forSampleRate:inputFormat.mSampleRate withInputChannels:inputFormat.mChannelsPerFrame withConfig:inputChannelConfig withMatrix:matrix];
if(!hrtf) {
return NO;
}
outputFormat = inputFormat;
outputFormat.mChannelsPerFrame = 2;
outputFormat.mBytesPerFrame = sizeof(float) * outputFormat.mChannelsPerFrame;
outputFormat.mBytesPerPacket = outputFormat.mBytesPerFrame * outputFormat.mFramesPerPacket;
outputChannelConfig = AudioChannelSideLeft | AudioChannelSideRight;
resetFilter = NO;
needPrefill = [hrtf needPrefill];
} else {
if(lastEnableHeadTracking) {
lastEnableHeadTracking = NO;
unregisterMotionListener();
}
referenceMatrixSet = NO;
hrtf = nil;
}
return YES;
}
- (void)fullShutdown {
hrtf = nil;
if(lastEnableHeadTracking) {
lastEnableHeadTracking = NO;
unregisterMotionListener();
}
resetFilter = NO;
}
- (BOOL)setup {
if(stopping)
return NO;
[self fullShutdown];
return [self fullInit];
}
- (void)cleanUp {
stopping = YES;
while(processEntered) {
usleep(500);
}
[self fullShutdown];
}
- (void)resetBuffer {
paused = YES;
while(processEntered) {
usleep(500);
}
[buffer reset];
[self fullShutdown];
paused = NO;
}
- (BOOL)paused {
return paused;
}
- (void)process {
while([self shouldContinue] == YES) {
if(paused || endOfStream) {
usleep(500);
continue;
}
@autoreleasepool {
AudioChunk *chunk = nil;
chunk = [self convert];
if(!chunk || ![chunk frameCount]) {
if([previousNode endOfStream] == YES) {
usleep(500);
endOfStream = YES;
continue;
}
if(paused) {
continue;
}
usleep(500);
} else {
[self writeChunk:chunk];
chunk = nil;
}
if(resetFilter || (!enableHrtf && hrtf)) {
[self fullShutdown];
}
}
}
}
- (AudioChunk *)convert {
if(stopping)
return nil;
processEntered = YES;
if(stopping || ([[previousNode buffer] isEmpty] && [previousNode endOfStream] == YES) || [self shouldContinue] == NO) {
processEntered = NO;
return nil;
}
if(![self peekFormat:&inputFormat channelConfig:&inputChannelConfig]) {
processEntered = NO;
return nil;
}
if(!inputFormat.mSampleRate ||
!inputFormat.mBitsPerChannel ||
!inputFormat.mChannelsPerFrame ||
!inputFormat.mBytesPerFrame ||
!inputFormat.mFramesPerPacket ||
!inputFormat.mBytesPerPacket) {
processEntered = NO;
return nil;
}
if((enableHrtf && !hrtf) ||
memcmp(&inputFormat, &lastInputFormat, sizeof(inputFormat)) != 0 ||
inputChannelConfig != lastInputChannelConfig) {
lastInputFormat = inputFormat;
lastInputChannelConfig = inputChannelConfig;
[self fullShutdown];
if(enableHrtf && ![self setup]) {
processEntered = NO;
return nil;
}
}
if(!hrtf) {
processEntered = NO;
return [self readChunk:4096];
}
AudioChunk *chunk = [self readChunkAsFloat32:4096];
if(!chunk || ![chunk frameCount]) {
processEntered = NO;
return nil;
}
if(rotationMatrixUpdated) {
rotationMatrixUpdated = NO;
simd_float4x4 mirrorTransform = {
simd_make_float4(-1.0, 0.0, 0.0, 0.0),
simd_make_float4(0.0, 1.0, 0.0, 0.0),
simd_make_float4(0.0, 0.0, 1.0, 0.0),
simd_make_float4(0.0, 0.0, 0.0, 1.0)
};
simd_float4x4 matrix = simd_mul(mirrorTransform, rotationMatrix);
matrix = simd_mul(matrix, referenceMatrix);
[hrtf reloadWithMatrix:matrix];
}
double streamTimestamp = [chunk streamTimestamp];
size_t frameCount = [chunk frameCount];
NSData *sampleData = [chunk removeSamples:frameCount];
if(needPrefill) {
size_t maxToUse = 4096 - needPrefill;
if(maxToUse > frameCount) {
maxToUse = frameCount;
}
size_t channels = inputFormat.mChannelsPerFrame;
memcpy(&prefillBuffer[needPrefill * channels], [sampleData bytes], maxToUse * sizeof(float) * channels);
lpc_extrapolate_bkwd(&prefillBuffer[needPrefill * channels], maxToUse, maxToUse, (int)channels, LPC_ORDER, needPrefill, &extrapolate_buffer, &extrapolate_buffer_size);
[hrtf process:&prefillBuffer[0] sampleCount:(int)needPrefill toBuffer:&outBuffer[0]];
needPrefill = 0;
}
[hrtf process:(const float *)[sampleData bytes] sampleCount:(int)frameCount toBuffer:&outBuffer[0]];
AudioChunk *outputChunk = [[AudioChunk alloc] init];
[outputChunk setFormat:outputFormat];
if(outputChannelConfig) {
[outputChunk setChannelConfig:outputChannelConfig];
}
if([chunk isHDCD]) [outputChunk setHDCD];
[outputChunk setStreamTimestamp:streamTimestamp];
[outputChunk setStreamTimeRatio:[chunk streamTimeRatio]];
[outputChunk assignSamples:&outBuffer[0] frameCount:frameCount];
processEntered = NO;
return outputChunk;
}
- (void)reportMotion:(simd_float4x4)matrix {
rotationMatrix = matrix;
if(!referenceMatrixSet) {
referenceMatrix = simd_inverse(matrix);
referenceMatrixSet = YES;
}
rotationMatrixUpdated = YES;
}
- (void)resetReferencePosition:(NSNotification *)notification {
referenceMatrixSet = NO;
}
@end

View file

@ -0,0 +1,32 @@
//
// DSPRubberbandNode.h
// CogAudio
//
// Created by Christopher Snowhill on 2/10/25.
//
#ifndef DSPRubberbandNode_h
#define DSPRubberbandNode_h
#import <CogAudio/DSPNode.h>
@interface DSPRubberbandNode : DSPNode {
}
- (id _Nullable)initWithController:(id _Nonnull)c previous:(id _Nullable)p latency:(double)latency;
- (BOOL)setup;
- (void)cleanUp;
- (void)resetBuffer;
- (BOOL)paused;
- (void)process;
- (AudioChunk * _Nullable)convert;
- (double)secondsBuffered;
@end
#endif /* DSPRubberbandNode_h */

View file

@ -0,0 +1,560 @@
//
// DSPRubberbandNode.m
// CogAudio Framework
//
// Created by Christopher Snowhill on 2/10/25.
//
#import <Foundation/Foundation.h>
#import <Accelerate/Accelerate.h>
#import "DSPRubberbandNode.h"
#import "Logging.h"
#import <rubberband/rubberband-c.h>
static void * kDSPRubberbandNodeContext = &kDSPRubberbandNodeContext;
@implementation DSPRubberbandNode {
BOOL enableRubberband;
RubberBandState ts;
RubberBandOptions tslastoptions, tsnewoptions;
size_t tschannels;
ssize_t blockSize, toDrop, samplesBuffered;
BOOL tsapplynewoptions;
BOOL tsrestartengine;
double tempo, pitch;
double lastTempo, lastPitch;
double countIn;
uint64_t countOut;
double streamTimestamp;
double streamTimeRatio;
BOOL isHDCD;
BOOL stopping, paused;
BOOL processEntered;
BOOL flushed;
BOOL observersapplied;
AudioStreamBasicDescription lastInputFormat;
AudioStreamBasicDescription inputFormat;
uint32_t lastInputChannelConfig, inputChannelConfig;
float *rsPtrs[32];
float rsInBuffer[4096 * 32];
float rsOutBuffer[65536 * 32];
}
- (id _Nullable)initWithController:(id _Nonnull)c previous:(id _Nullable)p latency:(double)latency {
self = [super initWithController:c previous:p latency:latency];
if(self) {
NSUserDefaults *defaults = [[NSUserDefaultsController sharedUserDefaultsController] defaults];
enableRubberband = ![[defaults stringForKey:@"rubberbandEngine"] isEqualToString:@"disabled"];
pitch = [defaults doubleForKey:@"pitch"];
tempo = [defaults doubleForKey:@"tempo"];
lastPitch = pitch;
lastTempo = tempo;
[self addObservers];
}
return self;
}
- (void)dealloc {
DLog(@"Rubber Band dealloc");
[self setShouldContinue:NO];
[self cleanUp];
[self removeObservers];
[super cleanUp];
}
- (void)addObservers {
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.pitch" options:(NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew) context:kDSPRubberbandNodeContext];
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.tempo" options:(NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew) context:kDSPRubberbandNodeContext];
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.rubberbandEngine" options:(NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew) context:kDSPRubberbandNodeContext];
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.rubberbandTransients" options:(NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew) context:kDSPRubberbandNodeContext];
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.rubberbandDetector" options:(NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew) context:kDSPRubberbandNodeContext];
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.rubberbandPhase" options:(NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew) context:kDSPRubberbandNodeContext];
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.rubberbandWindow" options:(NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew) context:kDSPRubberbandNodeContext];
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.rubberbandSmoothing" options:(NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew) context:kDSPRubberbandNodeContext];
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.rubberbandFormant" options:(NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew) context:kDSPRubberbandNodeContext];
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.rubberbandPitch" options:(NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew) context:kDSPRubberbandNodeContext];
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.rubberbandChannels" options:(NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew) context:kDSPRubberbandNodeContext];
observersapplied = YES;
}
- (void)removeObservers {
if(observersapplied) {
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.pitch" context:kDSPRubberbandNodeContext];
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.tempo" context:kDSPRubberbandNodeContext];
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.rubberbandEngine" context:kDSPRubberbandNodeContext];
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.rubberbandTransients" context:kDSPRubberbandNodeContext];
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.rubberbandDetector" context:kDSPRubberbandNodeContext];
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.rubberbandPhase" context:kDSPRubberbandNodeContext];
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.rubberbandWindow" context:kDSPRubberbandNodeContext];
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.rubberbandSmoothing" context:kDSPRubberbandNodeContext];
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.rubberbandFormant" context:kDSPRubberbandNodeContext];
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.rubberbandPitch" context:kDSPRubberbandNodeContext];
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.rubberbandChannels" context:kDSPRubberbandNodeContext];
observersapplied = NO;
}
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if(context != kDSPRubberbandNodeContext) {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
return;
}
if([keyPath isEqualToString:@"values.pitch"] ||
[keyPath isEqualToString:@"values.tempo"]) {
NSUserDefaults *defaults = [[NSUserDefaultsController sharedUserDefaultsController] defaults];
pitch = [defaults doubleForKey:@"pitch"];
tempo = [defaults doubleForKey:@"tempo"];
tsapplynewoptions = YES;
} else if([[keyPath substringToIndex:17] isEqualToString:@"values.rubberband"]) {
NSUserDefaults *defaults = [[NSUserDefaultsController sharedUserDefaultsController] defaults];
enableRubberband = ![[defaults stringForKey:@"rubberbandEngine"] isEqualToString:@"disabled"];
if(enableRubberband && ts) {
RubberBandOptions options = [self getRubberbandOptions];
RubberBandOptions changed = options ^ tslastoptions;
if(changed) {
BOOL engineR3 = !!(options & RubberBandOptionEngineFiner);
// Options which require a restart of the engine
const RubberBandOptions mustRestart = RubberBandOptionEngineFaster | RubberBandOptionEngineFiner | RubberBandOptionWindowStandard | RubberBandOptionWindowShort | RubberBandOptionWindowLong | RubberBandOptionSmoothingOff | RubberBandOptionSmoothingOn | (engineR3 ? RubberBandOptionPitchHighSpeed | RubberBandOptionPitchHighQuality | RubberBandOptionPitchHighConsistency : 0) | RubberBandOptionChannelsApart | RubberBandOptionChannelsTogether;
if(changed & mustRestart) {
tsrestartengine = YES;
} else {
tsnewoptions = options;
tsapplynewoptions = YES;
}
}
}
}
}
- (RubberBandOptions)getRubberbandOptions {
RubberBandOptions options = RubberBandOptionProcessRealTime;
NSUserDefaults *defaults = [[NSUserDefaultsController sharedUserDefaultsController] defaults];
NSString *value = [defaults stringForKey:@"rubberbandEngine"];
BOOL engineR3 = NO;
if([value isEqualToString:@"faster"]) {
options |= RubberBandOptionEngineFaster;
} else if([value isEqualToString:@"finer"]) {
options |= RubberBandOptionEngineFiner;
engineR3 = YES;
}
if(!engineR3) {
value = [defaults stringForKey:@"rubberbandTransients"];
if([value isEqualToString:@"crisp"]) {
options |= RubberBandOptionTransientsCrisp;
} else if([value isEqualToString:@"mixed"]) {
options |= RubberBandOptionTransientsMixed;
} else if([value isEqualToString:@"smooth"]) {
options |= RubberBandOptionTransientsSmooth;
}
value = [defaults stringForKey:@"rubberbandDetector"];
if([value isEqualToString:@"compound"]) {
options |= RubberBandOptionDetectorCompound;
} else if([value isEqualToString:@"percussive"]) {
options |= RubberBandOptionDetectorPercussive;
} else if([value isEqualToString:@"soft"]) {
options |= RubberBandOptionDetectorSoft;
}
value = [defaults stringForKey:@"rubberbandPhase"];
if([value isEqualToString:@"laminar"]) {
options |= RubberBandOptionPhaseLaminar;
} else if([value isEqualToString:@"independent"]) {
options |= RubberBandOptionPhaseIndependent;
}
}
value = [defaults stringForKey:@"rubberbandWindow"];
if([value isEqualToString:@"standard"]) {
options |= RubberBandOptionWindowStandard;
} else if([value isEqualToString:@"short"]) {
options |= RubberBandOptionWindowShort;
} else if([value isEqualToString:@"long"]) {
if(engineR3) {
options |= RubberBandOptionWindowStandard;
} else {
options |= RubberBandOptionWindowLong;
}
}
if(!engineR3) {
value = [defaults stringForKey:@"rubberbandSmoothing"];
if([value isEqualToString:@"off"]) {
options |= RubberBandOptionSmoothingOff;
} else if([value isEqualToString:@"on"]) {
options |= RubberBandOptionSmoothingOn;
}
}
value = [defaults stringForKey:@"rubberbandFormant"];
if([value isEqualToString:@"shifted"]) {
options |= RubberBandOptionFormantShifted;
} else if([value isEqualToString:@"preserved"]) {
options |= RubberBandOptionFormantPreserved;
}
value = [defaults stringForKey:@"rubberbandPitch"];
if([value isEqualToString:@"highspeed"]) {
options |= RubberBandOptionPitchHighSpeed;
} else if([value isEqualToString:@"highquality"]) {
options |= RubberBandOptionPitchHighQuality;
} else if([value isEqualToString:@"highconsistency"]) {
options |= RubberBandOptionPitchHighConsistency;
}
value = [defaults stringForKey:@"rubberbandChannels"];
if([value isEqualToString:@"apart"]) {
options |= RubberBandOptionChannelsApart;
} else if([value isEqualToString:@"together"]) {
options |= RubberBandOptionChannelsTogether;
}
return options;
}
- (BOOL)fullInit {
RubberBandOptions options = [self getRubberbandOptions];
tslastoptions = options;
tschannels = inputFormat.mChannelsPerFrame;
ts = rubberband_new(inputFormat.mSampleRate, (int)tschannels, options, 1.0 / tempo, pitch);
if(!ts)
return NO;
blockSize = rubberband_get_process_size_limit(ts);
toDrop = rubberband_get_start_delay(ts);
samplesBuffered = 0;
if(blockSize > 4096)
blockSize = 4096;
rubberband_set_max_process_size(ts, (unsigned int)blockSize);
for(size_t i = 0; i < 32; ++i) {
rsPtrs[i] = &rsInBuffer[4096 * i];
}
ssize_t toPad = rubberband_get_preferred_start_pad(ts);
if(toPad > 0) {
for(size_t i = 0; i < tschannels; ++i) {
memset(rsPtrs[i], 0, 4096 * sizeof(float));
}
while(toPad > 0) {
ssize_t p = toPad;
if(p > blockSize) p = blockSize;
rubberband_process(ts, (const float * const *)rsPtrs, (int)p, false);
toPad -= p;
}
}
tsapplynewoptions = NO;
tsrestartengine = NO;
flushed = NO;
countIn = 0.0;
countOut = 0;
return YES;
}
- (void)partialInit {
if(stopping || paused || !ts) return;
processEntered = YES;
RubberBandOptions changed = tslastoptions ^ tsnewoptions;
if(changed) {
tslastoptions = tsnewoptions;
BOOL engineR3 = !!(tsnewoptions & RubberBandOptionEngineFiner);
const RubberBandOptions transientsmask = RubberBandOptionTransientsCrisp | RubberBandOptionTransientsMixed | RubberBandOptionTransientsSmooth;
const RubberBandOptions detectormask = RubberBandOptionDetectorCompound | RubberBandOptionDetectorPercussive | RubberBandOptionDetectorSoft;
const RubberBandOptions phasemask = RubberBandOptionPhaseLaminar | RubberBandOptionPhaseIndependent;
const RubberBandOptions formantmask = RubberBandOptionFormantShifted | RubberBandOptionFormantPreserved;
const RubberBandOptions pitchmask = RubberBandOptionPitchHighSpeed | RubberBandOptionPitchHighQuality | RubberBandOptionPitchHighConsistency;
if(changed & transientsmask)
rubberband_set_transients_option(ts, tsnewoptions & transientsmask);
if(!engineR3) {
if(changed & detectormask)
rubberband_set_detector_option(ts, tsnewoptions & detectormask);
if(changed & phasemask)
rubberband_set_phase_option(ts, tsnewoptions & phasemask);
}
if(changed & formantmask)
rubberband_set_formant_option(ts, tsnewoptions & formantmask);
if(!engineR3 && (changed & pitchmask))
rubberband_set_pitch_option(ts, tsnewoptions & pitchmask);
}
if(fabs(pitch - lastPitch) > 1e-5 ||
fabs(tempo - lastTempo) > 1e-5) {
lastPitch = pitch;
lastTempo = tempo;
rubberband_set_pitch_scale(ts, pitch);
rubberband_set_time_ratio(ts, 1.0 / tempo);
}
tsapplynewoptions = NO;
processEntered = NO;
}
- (void)fullShutdown {
if(ts) {
rubberband_delete(ts);
ts = NULL;
}
}
- (BOOL)setup {
if(stopping)
return NO;
[self fullShutdown];
return [self fullInit];
}
- (void)cleanUp {
stopping = YES;
while(processEntered) {
usleep(500);
}
[self fullShutdown];
}
- (void)resetBuffer {
paused = YES;
while(processEntered) {
usleep(500);
}
[buffer reset];
[self fullShutdown];
paused = NO;
}
- (BOOL)paused {
return paused;
}
- (void)setPreviousNode:(id)p {
if(previousNode != p) {
paused = YES;
while(processEntered);
previousNode = p;
paused = NO;
}
}
- (void)setEndOfStream:(BOOL)e {
if(endOfStream && !e) {
[self fullShutdown];
}
[super setEndOfStream:e];
flushed = e;
}
- (void)process {
while([self shouldContinue] == YES) {
if(paused || endOfStream) {
usleep(500);
continue;
}
@autoreleasepool {
AudioChunk *chunk = nil;
chunk = [self convert];
if(!chunk || ![chunk frameCount]) {
if(!ts) {
flushed = previousNode && [[previousNode buffer] isEmpty] && [previousNode endOfStream] == YES;
}
if(flushed) {
usleep(500);
endOfStream = YES;
continue;
}
if(paused) {
continue;
}
usleep(500);
} else {
[self writeChunk:chunk];
chunk = nil;
}
if(!enableRubberband && ts) {
[self fullShutdown];
} else if(tsrestartengine) {
[self fullShutdown];
} else if(tsapplynewoptions) {
[self partialInit];
}
}
}
}
- (AudioChunk *)convert {
if(stopping)
return nil;
processEntered = YES;
if(stopping || flushed || !previousNode || ([[previousNode buffer] isEmpty] && [previousNode endOfStream] == YES) || [self shouldContinue] == NO) {
processEntered = NO;
return nil;
}
if(![self peekFormat:&inputFormat channelConfig:&inputChannelConfig]) {
processEntered = NO;
return nil;
}
if(!inputFormat.mSampleRate ||
!inputFormat.mBitsPerChannel ||
!inputFormat.mChannelsPerFrame ||
!inputFormat.mBytesPerFrame ||
!inputFormat.mFramesPerPacket ||
!inputFormat.mBytesPerPacket) {
processEntered = NO;
return nil;
}
if((enableRubberband && !ts) ||
memcmp(&inputFormat, &lastInputFormat, sizeof(inputFormat)) != 0 ||
inputChannelConfig != lastInputChannelConfig) {
lastInputFormat = inputFormat;
lastInputChannelConfig = inputChannelConfig;
[self fullShutdown];
if(enableRubberband && ![self setup]) {
processEntered = NO;
return nil;
}
}
if(!ts) {
processEntered = NO;
return [self readChunk:4096];
}
ssize_t samplesToProcess = rubberband_get_samples_required(ts);
if(samplesToProcess > blockSize)
samplesToProcess = blockSize;
int channels = (int)(inputFormat.mChannelsPerFrame);
if(samplesToProcess > 0) {
AudioChunk *chunk = [self readAndMergeChunksAsFloat32:samplesToProcess];
if(!chunk || ![chunk frameCount]) {
processEntered = NO;
return nil;
}
streamTimestamp = [chunk streamTimestamp];
streamTimeRatio = [chunk streamTimeRatio];
isHDCD = [chunk isHDCD];
size_t frameCount = [chunk frameCount];
countIn += ((double)frameCount) / tempo;
NSData *sampleData = [chunk removeSamples:frameCount];
for (size_t i = 0; i < channels; ++i) {
cblas_scopy((int)frameCount, ((const float *)[sampleData bytes]) + i, channels, rsPtrs[i], 1);
}
flushed = [[previousNode buffer] isEmpty] && [previousNode endOfStream] == YES;
int len = (int)frameCount;
rubberband_process(ts, (const float * const *)rsPtrs, len, flushed);
}
ssize_t samplesAvailable;
while(!stopping && (samplesAvailable = rubberband_available(ts)) > 0) {
if(toDrop > 0) {
ssize_t blockDrop = toDrop;
if(blockDrop > samplesAvailable) blockDrop = samplesAvailable;
if(blockDrop > blockSize) blockDrop = blockSize;
rubberband_retrieve(ts, (float * const *)rsPtrs, (int)blockDrop);
toDrop -= blockDrop;
continue;
}
ssize_t maxAvailable = 65536 - samplesBuffered;
ssize_t samplesOut = samplesAvailable;
if(samplesOut > maxAvailable) {
samplesOut = maxAvailable;
if(samplesOut <= 0) {
break;
}
}
if(samplesOut > blockSize) samplesOut = blockSize;
rubberband_retrieve(ts, (float * const *)rsPtrs, (int)samplesOut);
for(size_t i = 0; i < channels; ++i) {
cblas_scopy((int)samplesOut, rsPtrs[i], 1, &rsOutBuffer[samplesBuffered * channels + i], channels);
}
samplesBuffered += samplesOut;
}
if(flushed) {
if(samplesBuffered > 0) {
ssize_t ideal = (ssize_t)floor(countIn + 0.5);
if(countOut + samplesBuffered > ideal) {
// Rubber Band does not account for flushing duration in real time mode
samplesBuffered = ideal - countOut;
}
}
}
AudioChunk *outputChunk = nil;
if(samplesBuffered > 0) {
outputChunk = [[AudioChunk alloc] init];
[outputChunk setFormat:inputFormat];
if(inputChannelConfig) {
[outputChunk setChannelConfig:inputChannelConfig];
}
if(isHDCD) [outputChunk setHDCD];
[outputChunk setStreamTimestamp:streamTimestamp];
[outputChunk setStreamTimeRatio:streamTimeRatio * tempo];
[outputChunk assignSamples:&rsOutBuffer[0] frameCount:samplesBuffered];
countOut += samplesBuffered;
samplesBuffered = 0;
double chunkDuration = [outputChunk duration];
streamTimestamp += chunkDuration * [outputChunk streamTimeRatio];
}
processEntered = NO;
return outputChunk;
}
- (double)secondsBuffered {
double rbBuffered = 0.0;
if(ts) {
// We don't use Rubber Band's latency function, because at least in Cog's case,
// by the time we call this function, and also, because it doesn't account for
// how much audio will be lopped off at the end of the process.
//
// Tested once, this tends to be close to zero when actually called.
rbBuffered = countIn - (double)(countOut);
if(rbBuffered < 0) {
rbBuffered = 0.0;
} else {
rbBuffered /= inputFormat.mSampleRate;
}
}
return [buffer listDuration] + rbBuffered;
}
@end

View file

@ -9,11 +9,7 @@
#import <CoreAudio/CoreAudio.h>
#import <Foundation/Foundation.h>
#import "HeadphoneFilter.h"
@interface DownmixProcessor : NSObject {
HeadphoneFilter *hFilter;
AudioStreamBasicDescription inputFormat;
AudioStreamBasicDescription outputFormat;

View file

@ -149,10 +149,12 @@ static void downmix_to_stereo(const float *inBuffer, int channels, uint32_t conf
static void downmix_to_mono(const float *inBuffer, int channels, uint32_t config, float *outBuffer, size_t count) {
float tempBuffer[count * 2];
downmix_to_stereo(inBuffer, channels, config, tempBuffer, count);
inBuffer = tempBuffer;
channels = 2;
config = AudioConfigStereo;
if(channels > 2 || config != AudioConfigStereo) {
downmix_to_stereo(inBuffer, channels, config, tempBuffer, count);
inBuffer = tempBuffer;
channels = 2;
config = AudioConfigStereo;
}
cblas_scopy((int)count, inBuffer, 2, outBuffer, 1);
vDSP_vadd(outBuffer, 1, inBuffer + 1, 2, outBuffer, 1, count);
}
@ -275,93 +277,17 @@ static void *kDownmixProcessorContext = &kDownmixProcessorContext;
inConfig = iConfig;
outConfig = oConfig;
[self setupVirt];
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.headphoneVirtualization" options:0 context:kDownmixProcessorContext];
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.hrirPath" options:0 context:kDownmixProcessorContext];
}
return self;
}
- (void)dealloc {
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.headphoneVirtualization" context:kDownmixProcessorContext];
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.hrirPath" context:kDownmixProcessorContext];
}
- (void)setupVirt {
@synchronized(hFilter) {
hFilter = nil;
}
BOOL hVirt = [[[NSUserDefaultsController sharedUserDefaultsController] defaults] boolForKey:@"headphoneVirtualization"];
if(hVirt &&
outputFormat.mChannelsPerFrame >= 2 &&
(outConfig & AudioConfigStereo) == AudioConfigStereo &&
inputFormat.mChannelsPerFrame >= 1 &&
(inConfig & (AudioConfig7Point1 | AudioChannelBackCenter)) != 0) {
NSString *userPreset = [[[NSUserDefaultsController sharedUserDefaultsController] defaults] stringForKey:@"hrirPath"];
NSURL *presetUrl = nil;
if(userPreset && ![userPreset isEqualToString:@""]) {
presetUrl = [NSURL fileURLWithPath:userPreset];
if(![HeadphoneFilter validateImpulseFile:presetUrl])
presetUrl = nil;
}
if(!presetUrl) {
presetUrl = [[NSBundle mainBundle] URLForResource:@"gsx" withExtension:@"wv"];
if(![HeadphoneFilter validateImpulseFile:presetUrl])
presetUrl = nil;
}
if(presetUrl) {
@synchronized(hFilter) {
hFilter = [[HeadphoneFilter alloc] initWithImpulseFile:presetUrl forSampleRate:outputFormat.mSampleRate withInputChannels:inputFormat.mChannelsPerFrame withConfig:inConfig];
}
}
}
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if(context == kDownmixProcessorContext) {
DLog(@"SOMETHING CHANGED!");
if([keyPath isEqualToString:@"values.headphoneVirtualization"] ||
[keyPath isEqualToString:@"values.hrirPath"]) {
// Reset the converter, without rebuffering
[self setupVirt];
}
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
- (void)process:(const void *)inBuffer frameCount:(size_t)frames output:(void *)outBuffer {
@synchronized(hFilter) {
if(hFilter) {
uint32_t outChannels = outputFormat.mChannelsPerFrame;
if(outChannels > 2) {
float tempBuffer[frames * 2];
[hFilter process:(const float *)inBuffer sampleCount:frames toBuffer:&tempBuffer[0]];
cblas_scopy((int)frames, tempBuffer, 2, (float *)outBuffer, outChannels);
cblas_scopy((int)frames, tempBuffer + 1, 2, ((float *)outBuffer) + 1, outChannels);
for(size_t i = 2; i < outChannels; ++i) {
vDSP_vclr(((float *)outBuffer) + i, outChannels, (int)frames);
}
} else {
[hFilter process:(const float *)inBuffer sampleCount:frames toBuffer:(float *)outBuffer];
}
return;
}
}
if(inputFormat.mChannelsPerFrame > 2 && outConfig == AudioConfigStereo) {
if(inputFormat.mChannelsPerFrame == 2 && outConfig == AudioConfigStereo &&
inConfig == (AudioChannelSideLeft | AudioChannelSideRight)) {
// Workaround for HRTF output
memcpy(outBuffer, inBuffer, frames * outputFormat.mBytesPerPacket);
} else if(inputFormat.mChannelsPerFrame > 2 && outConfig == AudioConfigStereo) {
downmix_to_stereo((const float *)inBuffer, inputFormat.mChannelsPerFrame, inConfig, (float *)outBuffer, frames);
} else if(inputFormat.mChannelsPerFrame > 1 && outConfig == AudioConfigMono) {
downmix_to_mono((const float *)inBuffer, inputFormat.mChannelsPerFrame, inConfig, (float *)outBuffer, frames);

View file

@ -0,0 +1,36 @@
//
// FSurroundFilter.h
// CogAudio
//
// Created by Christopher Snowhill on 7/9/22.
//
#ifndef FSurroundFilter_h
#define FSurroundFilter_h
#import <Cocoa/Cocoa.h>
#import <stdint.h>
#define FSurroundChunkSize 4096
@interface FSurroundFilter : NSObject {
void *decoder;
void *params;
double srate;
uint32_t channelCount;
uint32_t channelConfig;
float tempBuffer[4096 * 2];
}
- (id)initWithSampleRate:(double)srate;
- (uint32_t)channelCount;
- (uint32_t)channelConfig;
- (double)srate;
- (void)process:(const float *)samplesIn output:(float *)samplesOut count:(uint32_t)count;
@end
#endif /* FSurround_h */

View file

@ -0,0 +1,156 @@
//
// FSurroundFilter.m
// CogAudio Framework
//
// Created by Christopher Snowhill on 7/9/22.
//
#import "FSurroundFilter.h"
#import "freesurround_decoder.h"
#import "AudioChunk.h"
#import <Accelerate/Accelerate.h>
#import <map>
#import <vector>
struct freesurround_params {
// the user-configurable parameters
float center_image, shift, depth, circular_wrap, focus, front_sep, rear_sep, bass_lo, bass_hi;
bool use_lfe;
channel_setup channels_fs; // FreeSurround channel setup
std::vector<unsigned> chanmap; // FreeSurround -> WFX channel index translation (derived data for faster lookup)
// construct with defaults
freesurround_params()
: center_image(0.7), shift(0), depth(1), circular_wrap(90), focus(0), front_sep(1), rear_sep(1),
bass_lo(40), bass_hi(90), use_lfe(false) {
set_channels_fs(cs_5point1);
}
// compute the WFX version of the channel setup code
unsigned channel_count() {
return (unsigned)chanmap.size();
}
unsigned channels_wfx() {
unsigned res = 0;
for(unsigned i = 0; i < chanmap.size(); res |= chanmap[i++]) {};
return res;
}
// assign a channel setup & recompute derived data
void set_channels_fs(channel_setup setup) {
channels_fs = setup;
chanmap.clear();
// Note: Because WFX does not define a few of the more exotic channels (side front left&right, side rear left&right, back center left&right),
// the side front/back channel pairs (both left and right sides, resp.) are mapped here onto foobar's top front/back channel pairs and the
// back (off-)center left/right channels are mapped onto foobar's top front center and top back center, respectively.
// Therefore, these speakers should be connected to those outputs instead.
std::map<channel_id, uint32_t> fs2wfx;
fs2wfx[ci_front_left] = AudioChannelFrontLeft;
fs2wfx[ci_front_center_left] = AudioChannelFrontCenterLeft;
fs2wfx[ci_front_center] = AudioChannelFrontCenter;
fs2wfx[ci_front_center_right] = AudioChannelFrontCenterRight;
fs2wfx[ci_front_right] = AudioChannelFrontRight;
fs2wfx[ci_side_front_left] = AudioChannelFrontLeft;
fs2wfx[ci_side_front_right] = AudioChannelTopFrontRight;
fs2wfx[ci_side_center_left] = AudioChannelSideLeft;
fs2wfx[ci_side_center_right] = AudioChannelSideRight;
fs2wfx[ci_side_back_left] = AudioChannelTopBackLeft;
fs2wfx[ci_side_back_right] = AudioChannelTopBackRight;
fs2wfx[ci_back_left] = AudioChannelBackLeft;
fs2wfx[ci_back_center_left] = AudioChannelTopFrontCenter;
fs2wfx[ci_back_center] = AudioChannelBackCenter;
fs2wfx[ci_back_center_right] = AudioChannelTopBackCenter;
fs2wfx[ci_back_right] = AudioChannelBackRight;
fs2wfx[ci_lfe] = AudioChannelLFE;
for(unsigned i = 0; i < freesurround_decoder::num_channels(channels_fs); i++)
chanmap.push_back(fs2wfx[freesurround_decoder::channel_at(channels_fs, i)]);
}
};
@implementation FSurroundFilter
- (id)initWithSampleRate:(double)srate {
self = [super init];
if(!self) return nil;
self->srate = srate;
freesurround_params *_params = new freesurround_params;
params = (void *)_params;
freesurround_decoder *_decoder = new freesurround_decoder(cs_5point1, 4096);
decoder = (void *)_decoder;
_decoder->circular_wrap(_params->circular_wrap);
_decoder->shift(_params->shift);
_decoder->depth(_params->depth);
_decoder->focus(_params->focus);
_decoder->center_image(_params->center_image);
_decoder->front_separation(_params->front_sep);
_decoder->rear_separation(_params->rear_sep);
_decoder->bass_redirection(_params->use_lfe);
_decoder->low_cutoff(_params->bass_lo / (srate / 2.0));
_decoder->high_cutoff(_params->bass_hi / (srate / 2.0));
channelCount = _params->channel_count();
channelConfig = _params->channels_wfx();
return self;
}
- (void)dealloc {
if(decoder) {
freesurround_decoder *_decoder = (freesurround_decoder *)decoder;
delete _decoder;
}
if(params) {
freesurround_params *_params = (freesurround_params *)params;
delete _params;
}
}
- (uint32_t)channelCount {
return channelCount;
}
- (uint32_t)channelConfig {
return channelConfig;
}
- (double)srate {
return srate;
}
- (void)process:(const float *)samplesIn output:(float *)samplesOut count:(uint32_t)count {
freesurround_params *_params = (freesurround_params *)params;
freesurround_decoder *_decoder = (freesurround_decoder *)decoder;
uint32_t zeroCount = 0;
if(count > 4096) {
zeroCount = count - 4096;
count = 4096;
}
if(count < 4096) {
cblas_scopy(count * 2, samplesIn, 1, &tempBuffer[0], 1);
vDSP_vclr(&tempBuffer[count * 2], 1, (4096 - count) * 2);
samplesIn = &tempBuffer[0];
}
float *src = _decoder->decode(samplesIn);
for(unsigned c = 0, num = channelCount; c < num; c++) {
unsigned idx = [AudioChunk channelIndexFromConfig:channelConfig forFlag:_params->chanmap[c]];
cblas_scopy(count, src + c, num, samplesOut + idx, num);
if(zeroCount) {
vDSP_vclr(samplesOut + idx + count, num, zeroCount);
}
}
}
@end

View file

@ -0,0 +1,46 @@
//
// HeadphoneFilter.h
// CogAudio Framework
//
// Created by Christopher Snowhill on 1/24/22.
//
#ifndef HeadphoneFilter_h
#define HeadphoneFilter_h
#import <Accelerate/Accelerate.h>
#import <Cocoa/Cocoa.h>
#import <simd/simd.h>
@interface HeadphoneFilter : NSObject {
NSURL *URL;
int bufferSize;
int paddedBufferSize;
double sampleRate;
int channelCount;
uint32_t config;
float **mirroredImpulseResponses;
float **prevInputs;
float *paddedSignal[2];
}
+ (BOOL)validateImpulseFile:(NSURL *)url;
- (id)initWithImpulseFile:(NSURL *)url forSampleRate:(double)sampleRate withInputChannels:(int)channels withConfig:(uint32_t)config withMatrix:(simd_float4x4)matrix;
- (void)reloadWithMatrix:(simd_float4x4)matrix;
- (void)process:(const float *)inBuffer sampleCount:(int)count toBuffer:(float *)outBuffer;
- (void)reset;
- (size_t)needPrefill;
@end
#endif /* HeadphoneFilter_h */

View file

@ -0,0 +1,386 @@
//
// HeadphoneFilter.m
// CogAudio Framework
//
// Created by Christopher Snowhill on 1/24/22.
//
#import "HeadphoneFilter.h"
#import "AudioChunk.h"
#import "AudioDecoder.h"
#import "AudioSource.h"
#import <stdlib.h>
#import <fstream>
#import <soxr.h>
#import "HrtfData.h"
#import "Logging.h"
typedef struct speakerPosition {
float elevation;
float azimuth;
float distance;
} speakerPosition;
#define DEGREES(x) ((x)*M_PI / 180.0)
static const speakerPosition speakerPositions[18] = {
{ .elevation = DEGREES(0.0), .azimuth = DEGREES(-30.0), .distance = 1.0 },
{ .elevation = DEGREES(0.0), .azimuth = DEGREES(+30.0), .distance = 1.0 },
{ .elevation = DEGREES(0.0), .azimuth = DEGREES(0.0), .distance = 1.0 },
{ .elevation = DEGREES(0.0), .azimuth = DEGREES(0.0), .distance = 1.0 },
{ .elevation = DEGREES(0.0), .azimuth = DEGREES(-135.0), .distance = 1.0 },
{ .elevation = DEGREES(0.0), .azimuth = DEGREES(+135.0), .distance = 1.0 },
{ .elevation = DEGREES(0.0), .azimuth = DEGREES(-15.0), .distance = 1.0 },
{ .elevation = DEGREES(0.0), .azimuth = DEGREES(+15.0), .distance = 1.0 },
{ .elevation = DEGREES(0.0), .azimuth = DEGREES(-180.0), .distance = 1.0 },
{ .elevation = DEGREES(0.0), .azimuth = DEGREES(-90.0), .distance = 1.0 },
{ .elevation = DEGREES(0.0), .azimuth = DEGREES(+90.0), .distance = 1.0 },
{ .elevation = DEGREES(+90.0), .azimuth = DEGREES(0.0), .distance = 1.0 },
{ .elevation = DEGREES(+45.0), .azimuth = DEGREES(-30.0), .distance = 1.0 },
{ .elevation = DEGREES(+45.0), .azimuth = DEGREES(0.0), .distance = 1.0 },
{ .elevation = DEGREES(+45.0), .azimuth = DEGREES(+30.0), .distance = 1.0 },
{ .elevation = DEGREES(+45.0), .azimuth = DEGREES(-135.0), .distance = 1.0 },
{ .elevation = DEGREES(+45.0), .azimuth = DEGREES(0.0), .distance = 1.0 },
{ .elevation = DEGREES(+45.0), .azimuth = DEGREES(+135.0), .distance = 1.0 }
};
static simd_float4x4 matX(float theta) {
simd_float4x4 mat = {
simd_make_float4(1.0f, 0.0f, 0.0f, 0.0f),
simd_make_float4(0.0f, cosf(theta), -sinf(theta), 0.0f),
simd_make_float4(0.0f, sinf(theta), cosf(theta), 0.0f),
simd_make_float4(0.0f, 0.0f, 0.0f, 1.0f)
};
return mat;
};
static simd_float4x4 matY(float theta) {
simd_float4x4 mat = {
simd_make_float4(cosf(theta), 0.0f, sinf(theta), 0.0f),
simd_make_float4(0.0f, 1.0f, 0.0f, 0.0f),
simd_make_float4(-sinf(theta), 0.0f, cosf(theta), 0.0f),
simd_make_float4(0.0f, 0.0f, 0.0f, 1.0f)
};
return mat;
}
#if 0
static simd_float4x4 matZ(float theta) {
simd_float4x4 mat = {
simd_make_float4(cosf(theta), -sinf(theta), 0.0f, 0.0f),
simd_make_float4(sinf(theta), cosf(theta), 0.0f, 0.0f),
simd_make_float4(0.0f, 0.0f, 1.0f, 0.0f),
simd_make_float4(0.0f, 0.0f, 0.0f, 1.0f)
};
return mat;
};
#endif
static void transformPosition(float &elevation, float &azimuth, const simd_float4x4 &matrix) {
simd_float4x4 mat_x = matX(azimuth);
simd_float4x4 mat_y = matY(elevation);
//simd_float4x4 mat_z = matrix_identity_float4x4;
simd_float4x4 offsetMatrix = simd_mul(mat_x, mat_y);
//offsetMatrix = simd_mul(offsetMatrix, mat_z);
offsetMatrix = simd_mul(offsetMatrix, matrix);
double sy = sqrt(offsetMatrix.columns[0].x * offsetMatrix.columns[0].x + offsetMatrix.columns[1].x * offsetMatrix.columns[1].x);
bool singular = sy < 1e-6; // If
float x, y/*, z*/;
if(!singular) {
x = atan2(offsetMatrix.columns[2].y, offsetMatrix.columns[2].z);
y = atan2(-offsetMatrix.columns[2].x, sy);
//z = atan2(offsetMatrix.columns[1].x, offsetMatrix.columns[0].x);
} else {
x = atan2(-offsetMatrix.columns[1].z, offsetMatrix.columns[1].y);
y = atan2(-offsetMatrix.columns[2].x, sy);
//z = 0;
}
elevation = y;
azimuth = x;
if(elevation < (M_PI * (-0.5))) {
elevation = (M_PI * (-0.5));
} else if(elevation > M_PI * 0.5) {
elevation = M_PI * 0.5;
}
while(azimuth < (M_PI * (-2.0))) {
azimuth += M_PI * 2.0;
}
while(azimuth > M_PI * 2.0) {
azimuth -= M_PI * 2.0;
}
}
@interface impulseSetCache : NSObject {
NSURL *URL;
HrtfData *data;
}
+ (impulseSetCache *)sharedController;
- (void)getImpulse:(NSURL *)url outImpulse:(float **)outImpulse outSampleCount:(int *)outSampleCount sampleRate:(double)sampleRate channelCount:(int)channelCount channelConfig:(uint32_t)channelConfig withMatrix:(simd_float4x4)matrix;
@end
@implementation impulseSetCache
static impulseSetCache *_sharedController = nil;
+ (impulseSetCache *)sharedController {
@synchronized(self) {
if(!_sharedController) {
_sharedController = [[impulseSetCache alloc] init];
}
}
return _sharedController;
}
- (id)init {
self = [super init];
if(self) {
data = NULL;
}
return self;
}
- (void)dealloc {
delete data;
}
- (void)getImpulse:(NSURL *)url outImpulse:(float **)outImpulse outSampleCount:(int *)outSampleCount sampleRate:(double)sampleRate channelCount:(int)channelCount channelConfig:(uint32_t)channelConfig withMatrix:(simd_float4x4)matrix {
double sampleRateOfSource = 0;
int sampleCount = 0;
if(!data || ![url isEqualTo:URL]) {
delete data;
data = NULL;
URL = url;
NSString *filePath = [url path];
try {
std::ifstream file([filePath UTF8String], std::fstream::binary);
if(!file.is_open()) {
throw std::logic_error("Cannot open file.");
}
data = new HrtfData(file);
file.close();
} catch(std::exception &e) {
ALog(@"Exception caught: %s", e.what());
}
}
try {
soxr_quality_spec_t q_spec = soxr_quality_spec(SOXR_HQ, 0);
soxr_io_spec_t io_spec = soxr_io_spec(SOXR_FLOAT32_I, SOXR_FLOAT32_I);
soxr_runtime_spec_t runtime_spec = soxr_runtime_spec(0);
bool resampling;
sampleRateOfSource = data->get_sample_rate();
resampling = !!(fabs(sampleRateOfSource - sampleRate) > 1e-6);
uint32_t sampleCountResampled;
uint32_t sampleCountExact = data->get_response_length();
sampleCount = sampleCountExact + ((data->get_longest_delay() + 2) >> 2);
uint32_t actualSampleCount = sampleCount;
if(resampling) {
sampleCountResampled = (uint32_t)(((double)sampleCountExact) * sampleRate / sampleRateOfSource);
actualSampleCount = (uint32_t)(((double)actualSampleCount) * sampleRate / sampleRateOfSource);
io_spec.scale = sampleRateOfSource / sampleRate;
}
actualSampleCount = (actualSampleCount + 15) & ~15;
*outImpulse = (float *)calloc(sizeof(float), actualSampleCount * channelCount * 2);
if(!*outImpulse) {
throw std::bad_alloc();
}
float *hrtfData = *outImpulse;
for(uint32_t i = 0; i < channelCount; ++i) {
uint32_t channelFlag = [AudioChunk extractChannelFlag:i fromConfig:channelConfig];
uint32_t channelNumber = [AudioChunk findChannelIndex:channelFlag];
if(channelNumber < 18) {
const speakerPosition &speaker = speakerPositions[channelNumber];
DirectionData hrtfLeft;
DirectionData hrtfRight;
float azimuth = speaker.azimuth;
float elevation = speaker.elevation;
transformPosition(elevation, azimuth, matrix);
data->get_direction_data(elevation, azimuth, speaker.distance, hrtfLeft, hrtfRight);
if(resampling) {
ssize_t leftDelay = (ssize_t)((double)(hrtfLeft.delay) * 0.25 * sampleRate / sampleRateOfSource);
ssize_t rightDelay = (ssize_t)((double)(hrtfRight.delay) * 0.25 * sampleRate / sampleRateOfSource);
soxr_oneshot(sampleRateOfSource, sampleRate, 1, &hrtfLeft.impulse_response[0], sampleCountExact, NULL, &hrtfData[leftDelay + actualSampleCount * i * 2], sampleCountResampled, NULL, &io_spec, &q_spec, &runtime_spec);
soxr_oneshot(sampleRateOfSource, sampleRate, 1, &hrtfRight.impulse_response[0], sampleCountExact, NULL, &hrtfData[rightDelay + actualSampleCount * (i * 2 + 1)], sampleCountResampled, NULL, &io_spec, &q_spec, &runtime_spec);
} else {
cblas_scopy(sampleCountExact, &hrtfLeft.impulse_response[0], 1, &hrtfData[((hrtfLeft.delay + 2) >> 2) + actualSampleCount * i * 2], 1);
cblas_scopy(sampleCountExact, &hrtfRight.impulse_response[0], 1, &hrtfData[((hrtfRight.delay + 2) >> 2) + actualSampleCount * (i * 2 + 1)], 1);
}
}
}
*outSampleCount = actualSampleCount;
} catch(std::exception &e) {
ALog(@"Exception caught: %s", e.what());
}
}
@end
@implementation HeadphoneFilter
+ (BOOL)validateImpulseFile:(NSURL *)url {
NSString *filePath = [url path];
try {
std::ifstream file([filePath UTF8String], std::fstream::binary);
if(!file.is_open()) {
throw std::logic_error("Cannot open file.");
}
HrtfData data(file);
file.close();
return YES;
} catch(std::exception &e) {
ALog(@"Exception thrown: %s", e.what());
return NO;
}
}
- (id)initWithImpulseFile:(NSURL *)url forSampleRate:(double)sampleRate withInputChannels:(int)channels withConfig:(uint32_t)config withMatrix:(simd_float4x4)matrix {
self = [super init];
if(self) {
URL = url;
self->sampleRate = sampleRate;
channelCount = channels;
self->config = config;
float *impulseBuffer = NULL;
int sampleCount = 0;
[[impulseSetCache sharedController] getImpulse:url outImpulse:&impulseBuffer outSampleCount:&sampleCount sampleRate:sampleRate channelCount:channels channelConfig:config withMatrix:matrix];
if(!impulseBuffer) {
return nil;
}
mirroredImpulseResponses = (float **)calloc(sizeof(float *), channelCount * 2);
if(!mirroredImpulseResponses) {
free(impulseBuffer);
return nil;
}
for(int i = 0; i < channelCount * 2; ++i) {
mirroredImpulseResponses[i] = &impulseBuffer[sampleCount * i];
vDSP_vrvrs(mirroredImpulseResponses[i], 1, sampleCount);
}
paddedBufferSize = sampleCount;
paddedSignal[0] = (float *)calloc(sizeof(float), paddedBufferSize * 2);
if(!paddedSignal[0]) {
return nil;
}
paddedSignal[1] = paddedSignal[0] + paddedBufferSize;
prevInputs = (float **)calloc(channels, sizeof(float *));
if(!prevInputs)
return nil;
prevInputs[0] = (float *)calloc(sizeof(float), sampleCount * channelCount);
if(!prevInputs[0])
return nil;
for(int i = 1; i < channels; ++i) {
prevInputs[i] = prevInputs[i - 1] + sampleCount;
}
}
return self;
}
- (void)dealloc {
if(paddedSignal[0]) {
free(paddedSignal[0]);
}
if(prevInputs) {
if(prevInputs[0]) {
free(prevInputs[0]);
}
free(prevInputs);
}
if(mirroredImpulseResponses) {
if(mirroredImpulseResponses[0]) {
free(mirroredImpulseResponses[0]);
}
free(mirroredImpulseResponses);
}
}
- (void)reloadWithMatrix:(simd_float4x4)matrix {
@synchronized (self) {
if(!mirroredImpulseResponses[0]) {
return;
}
free(mirroredImpulseResponses[0]);
float *impulseBuffer = NULL;
int sampleCount = 0;
[[impulseSetCache sharedController] getImpulse:URL outImpulse:&impulseBuffer outSampleCount:&sampleCount sampleRate:sampleRate channelCount:channelCount channelConfig:config withMatrix:matrix];
for(int i = 0; i < channelCount * 2; ++i) {
mirroredImpulseResponses[i] = &impulseBuffer[sampleCount * i];
vDSP_vrvrs(mirroredImpulseResponses[i], 1, sampleCount);
}
}
}
- (void)process:(const float *)inBuffer sampleCount:(int)count toBuffer:(float *)outBuffer {
@synchronized (self) {
int sampleCount = paddedBufferSize;
while(count > 0) {
float left = 0, right = 0;
for(int i = 0; i < channelCount; ++i) {
float thisleft, thisright;
vDSP_vmul(prevInputs[i], 1, mirroredImpulseResponses[i * 2], 1, paddedSignal[0], 1, sampleCount);
vDSP_vmul(prevInputs[i], 1, mirroredImpulseResponses[i * 2 + 1], 1, paddedSignal[1], 1, sampleCount);
vDSP_sve(paddedSignal[0], 1, &thisleft, sampleCount);
vDSP_sve(paddedSignal[1], 1, &thisright, sampleCount);
left += thisleft;
right += thisright;
memmove(prevInputs[i], prevInputs[i] + 1, sizeof(float) * (sampleCount - 1));
prevInputs[i][sampleCount - 1] = *inBuffer++;
}
outBuffer[0] = left;
outBuffer[1] = right;
outBuffer += 2;
--count;
}
}
}
- (void)reset {
for(int i = 0; i < channelCount; ++i) {
vDSP_vclr(prevInputs[i], 1, paddedBufferSize);
}
}
- (size_t)needPrefill {
return paddedBufferSize;
}
@end

26
Audio/Chain/DSPNode.h Normal file
View file

@ -0,0 +1,26 @@
//
// DSPNode.h
// CogAudio
//
// Created by Christopher Snowhill on 2/10/25.
//
#ifndef DSPNode_h
#define DSPNode_h
#import <CogAudio/Node.h>
@interface DSPNode : Node {
}
- (id _Nullable)initWithController:(id _Nonnull)c previous:(id _Nullable)p latency:(double)latency;
- (void)threadEntry:(id _Nullable)arg;
- (void)setShouldContinue:(BOOL)s;
- (double)secondsBuffered;
@end
#endif /* DSPNode_h */

76
Audio/Chain/DSPNode.m Normal file
View file

@ -0,0 +1,76 @@
//
// DSPNode.m
// CogAudio Framework
//
// Created by Christopher Snowhill on 2/10/25.
//
#import <Foundation/Foundation.h>
#import "DSPNode.h"
@implementation DSPNode {
BOOL threadTerminated;
}
- (id _Nullable)initWithController:(id _Nonnull)c previous:(id _Nullable)p latency:(double)latency {
self = [super init];
if(self) {
buffer = [[ChunkList alloc] initWithMaximumDuration:latency];
writeSemaphore = [[Semaphore alloc] init];
readSemaphore = [[Semaphore alloc] init];
accessLock = [[NSLock alloc] init];
initialBufferFilled = NO;
controller = c;
endOfStream = NO;
shouldContinue = YES;
nodeChannelConfig = 0;
nodeLossless = NO;
durationPrebuffer = latency * 0.25;
inWrite = NO;
inPeek = NO;
inRead = NO;
inMerge = NO;
[self setPreviousNode:p];
#ifdef LOG_CHAINS
[self initLogFiles];
#endif
}
return self;
}
// DSP threads buffer for low latency, and therefore should have high priority
- (void)threadEntry:(id _Nullable)arg {
@autoreleasepool {
NSThread *currentThread = [NSThread currentThread];
[currentThread setThreadPriority:0.75];
[currentThread setQualityOfService:NSQualityOfServiceUserInitiated];
threadTerminated = NO;
[self process];
threadTerminated = YES;
}
}
- (void)setShouldContinue:(BOOL)s {
BOOL currentShouldContinue = shouldContinue;
shouldContinue = s;
if(!currentShouldContinue && s && threadTerminated) {
[self launchThread];
}
}
- (double)secondsBuffered {
return [buffer listDuration];
}
@end

View file

@ -1,47 +0,0 @@
//
// HeadphoneFilter.h
// CogAudio Framework
//
// Created by Christopher Snowhill on 1/24/22.
//
#ifndef HeadphoneFilter_h
#define HeadphoneFilter_h
#import <Accelerate/Accelerate.h>
#import <Cocoa/Cocoa.h>
@interface HeadphoneFilter : NSObject {
vDSP_DFT_Setup dftSetupF;
vDSP_DFT_Setup dftSetupB;
size_t fftSize;
size_t fftSizeOver2;
size_t bufferSize;
size_t paddedBufferSize;
size_t channelCount;
DSPSplitComplex signal_fft;
DSPSplitComplex input_filtered_signal_per_channel[2];
DSPSplitComplex input_filtered_signal_totals[2];
DSPSplitComplex *impulse_responses;
float **prevInputs;
float *left_result;
float *right_result;
float *paddedSignal;
}
+ (BOOL)validateImpulseFile:(NSURL *)url;
- (id)initWithImpulseFile:(NSURL *)url forSampleRate:(double)sampleRate withInputChannels:(size_t)channels withConfig:(uint32_t)config;
- (void)process:(const float *)inBuffer sampleCount:(size_t)count toBuffer:(float *)outBuffer;
- (void)reset;
@end
#endif /* HeadphoneFilter_h */

View file

@ -1,678 +0,0 @@
//
// HeadphoneFilter.m
// CogAudio Framework
//
// Created by Christopher Snowhill on 1/24/22.
//
#import "HeadphoneFilter.h"
#import "AudioChunk.h"
#import "AudioDecoder.h"
#import "AudioSource.h"
#import <stdlib.h>
#import "r8bstate.h"
#import "lpc.h"
#import "util.h"
@interface impulseCacheObject : NSObject {
}
@property NSURL *URL;
@property int sampleCount;
@property int channelCount;
@property double sampleRate;
@property double targetSampleRate;
@property NSData *data;
@end
@implementation impulseCacheObject
@synthesize URL;
@synthesize sampleCount;
@synthesize channelCount;
@synthesize sampleRate;
@synthesize targetSampleRate;
@synthesize data;
@end
@interface impulseCache : NSObject {
}
@property NSMutableArray<impulseCacheObject *> *cacheObjects;
+ (impulseCache *)sharedController;
- (const float *)getImpulse:(NSURL *)url sampleCount:(int *)sampleCount channelCount:(int *)channelCount sampleRate:(double)sampleRate;
@end
// Apparently _mm_malloc is Intel-only on newer macOS targets, so use supported posix_memalign
static void *_memalign_malloc(size_t size, size_t align) {
void *ret = NULL;
if(posix_memalign(&ret, align, size) != 0) {
return NULL;
}
return ret;
}
@implementation impulseCache
static impulseCache *_sharedController = nil;
+ (impulseCache *)sharedController {
@synchronized(self) {
if(!_sharedController) {
_sharedController = [[impulseCache alloc] init];
}
}
return _sharedController;
}
- (id)init {
self = [super init];
if(self) {
self.cacheObjects = [[NSMutableArray alloc] init];
}
return self;
}
- (impulseCacheObject *)addImpulse:(NSURL *)url sampleCount:(int)sampleCount channelCount:(int)channelCount originalSampleRate:(double)originalSampleRate targetSampleRate:(double)targetSampleRate impulseBuffer:(const float *)impulseBuffer {
impulseCacheObject *obj = [[impulseCacheObject alloc] init];
obj.URL = url;
obj.sampleCount = sampleCount;
obj.channelCount = channelCount;
obj.sampleRate = originalSampleRate;
obj.targetSampleRate = targetSampleRate;
obj.data = [NSData dataWithBytes:impulseBuffer length:(sampleCount * channelCount * sizeof(float))];
@synchronized(self.cacheObjects) {
[self.cacheObjects addObject:obj];
}
return obj;
}
- (const float *)getImpulse:(NSURL *)url sampleCount:(int *)retSampleCount channelCount:(int *)retImpulseChannels sampleRate:(double)sampleRate {
BOOL impulseFound = NO;
const float *impulseData = NULL;
double sampleRateOfSource = 0;
int sampleCount = 0;
int impulseChannels = 0;
impulseCacheObject *cacheObject = nil;
@synchronized(self.cacheObjects) {
for(impulseCacheObject *obj in self.cacheObjects) {
if([obj.URL isEqualTo:url] &&
obj.targetSampleRate == sampleRate) {
*retSampleCount = obj.sampleCount;
*retImpulseChannels = obj.channelCount;
return (const float *)[obj.data bytes];
}
}
for(impulseCacheObject *obj in self.cacheObjects) {
if([obj.URL isEqualTo:url] &&
obj.sampleRate == obj.targetSampleRate) {
impulseData = (const float *)[obj.data bytes];
sampleCount = obj.sampleCount;
impulseChannels = obj.channelCount;
sampleRateOfSource = obj.sampleRate;
impulseFound = YES;
break;
}
}
}
if(!impulseFound) {
id<CogSource> source = [AudioSource audioSourceForURL:url];
if(!source)
return NULL;
if(![source open:url])
return NULL;
id<CogDecoder> decoder = [AudioDecoder audioDecoderForSource:source];
if(decoder == nil) {
[source close];
source = nil;
return NULL;
}
if(![decoder open:source]) {
decoder = nil;
[source close];
source = nil;
return NULL;
}
NSDictionary *properties = [decoder properties];
sampleRateOfSource = [[properties objectForKey:@"sampleRate"] floatValue];
sampleCount = [[properties objectForKey:@"totalFrames"] intValue];
impulseChannels = [[properties objectForKey:@"channels"] intValue];
if([[properties objectForKey:@"floatingPoint"] boolValue] != YES ||
[[properties objectForKey:@"bitsPerSample"] intValue] != 32 ||
!([[properties objectForKey:@"endian"] isEqualToString:@"host"] ||
[[properties objectForKey:@"endian"] isEqualToString:@"little"]) ||
(impulseChannels != 14 && impulseChannels != 7)) {
[decoder close];
decoder = nil;
[source close];
source = nil;
return NULL;
}
float *impulseBuffer = (float *)_memalign_malloc(sampleCount * sizeof(float) * impulseChannels, 16);
if(!impulseBuffer) {
[decoder close];
decoder = nil;
[source close];
source = nil;
return NULL;
}
if([decoder readAudio:impulseBuffer frames:sampleCount] != sampleCount) {
free(impulseBuffer);
[decoder close];
decoder = nil;
[source close];
source = nil;
return NULL;
}
[decoder close];
decoder = nil;
[source close];
source = nil;
cacheObject = [self addImpulse:url sampleCount:sampleCount channelCount:impulseChannels originalSampleRate:sampleRateOfSource targetSampleRate:sampleRateOfSource impulseBuffer:impulseBuffer];
free(impulseBuffer);
impulseData = (const float *)[cacheObject.data bytes];
}
if(sampleRateOfSource != sampleRate) {
double sampleRatio = sampleRate / sampleRateOfSource;
int resampledCount = (int)ceil((double)sampleCount * sampleRatio);
r8bstate *_r8bstate = new r8bstate(impulseChannels, 1024, sampleRateOfSource, sampleRate);
unsigned long PRIME_LEN_ = MAX(sampleRateOfSource / 20, 1024u);
PRIME_LEN_ = MIN(PRIME_LEN_, 16384u);
PRIME_LEN_ = MAX(PRIME_LEN_, 2 * LPC_ORDER + 1);
unsigned int N_samples_to_add_ = sampleRateOfSource;
unsigned int N_samples_to_drop_ = sampleRate;
samples_len(&N_samples_to_add_, &N_samples_to_drop_, 20, 8192u);
int resamplerLatencyIn = (int)N_samples_to_add_;
int resamplerLatencyOut = (int)N_samples_to_drop_;
float *tempImpulse = (float *)_memalign_malloc((sampleCount + resamplerLatencyIn * 2 + 1024) * sizeof(float) * impulseChannels, 16);
if(!tempImpulse) {
return nil;
}
resampledCount += resamplerLatencyOut * 2 + 1024;
float *resampledImpulse = (float *)_memalign_malloc(resampledCount * sizeof(float) * impulseChannels, 16);
if(!resampledImpulse) {
free(tempImpulse);
return nil;
}
size_t prime = MIN(sampleCount, PRIME_LEN_);
void *extrapolate_buffer = NULL;
size_t extrapolate_buffer_size = 0;
memcpy(tempImpulse + resamplerLatencyIn * impulseChannels, impulseData, sampleCount * sizeof(float) * impulseChannels);
lpc_extrapolate_bkwd(tempImpulse + N_samples_to_add_ * impulseChannels, sampleCount, prime, impulseChannels, LPC_ORDER, N_samples_to_add_, &extrapolate_buffer, &extrapolate_buffer_size);
lpc_extrapolate_fwd(tempImpulse + N_samples_to_add_ * impulseChannels, sampleCount, prime, impulseChannels, LPC_ORDER, N_samples_to_add_, &extrapolate_buffer, &extrapolate_buffer_size);
free(extrapolate_buffer);
size_t inputDone = 0;
size_t outputDone = 0;
outputDone = _r8bstate->resample(tempImpulse, sampleCount + N_samples_to_add_ * 2, &inputDone, resampledImpulse, resampledCount);
free(tempImpulse);
if(outputDone < resampledCount) {
outputDone += _r8bstate->flush(resampledImpulse + outputDone * impulseChannels, resampledCount - outputDone);
}
delete _r8bstate;
outputDone -= N_samples_to_drop_ * 2;
// Do this instead of the memmove
float *resampledImpulseData = resampledImpulse + N_samples_to_drop_ * impulseChannels;
/*memmove(resampledImpulse, resampledImpulse + N_samples_to_drop_ * impulseChannels, outputDone * sizeof(float) * impulseChannels);*/
sampleCount = (int)outputDone;
// Normalize resampled impulse by sample ratio
float fSampleRatio = (float)sampleRatio;
vDSP_vsdiv(resampledImpulseData, 1, &fSampleRatio, resampledImpulseData, 1, sampleCount * impulseChannels);
cacheObject = [self addImpulse:url sampleCount:sampleCount channelCount:impulseChannels originalSampleRate:sampleRateOfSource targetSampleRate:sampleRate impulseBuffer:resampledImpulseData];
free(resampledImpulse);
impulseData = (const float *)[cacheObject.data bytes];
}
*retSampleCount = sampleCount;
*retImpulseChannels = impulseChannels;
return impulseData;
}
@end
@implementation HeadphoneFilter
enum {
speaker_is_back_center = -1,
speaker_not_present = -2,
};
static const uint32_t max_speaker_index = 10;
static const int8_t speakers_to_hesuvi_7[11][2] = {
// front left
{ 0, 1 },
// front right
{ 1, 0 },
// front center
{ 6, 6 },
// lfe
{ 6, 6 },
// back left
{ 4, 5 },
// back right
{ 5, 4 },
// front center left
{ speaker_not_present, speaker_not_present },
// front center right
{ speaker_not_present, speaker_not_present },
// back center
{ speaker_is_back_center, speaker_is_back_center },
// side left
{ 2, 3 },
// side right
{ 3, 2 }
};
static const int8_t speakers_to_hesuvi_14[11][2] = {
// front left
{ 0, 1 },
// front right
{ 8, 7 },
// front center
{ 6, 13 },
// lfe
{ 6, 13 },
// back left
{ 4, 5 },
// back right
{ 12, 11 },
// front center left
{ speaker_not_present, speaker_not_present },
// front center right
{ speaker_not_present, speaker_not_present },
// back center
{ speaker_is_back_center, speaker_is_back_center },
// side left
{ 2, 3 },
// side right
{ 10, 9 }
};
+ (BOOL)validateImpulseFile:(NSURL *)url {
id<CogSource> source = [AudioSource audioSourceForURL:url];
if(!source)
return NO;
if(![source open:url])
return NO;
id<CogDecoder> decoder = [AudioDecoder audioDecoderForSource:source];
if(decoder == nil) {
[source close];
source = nil;
return NO;
}
if(![decoder open:source]) {
decoder = nil;
[source close];
source = nil;
return NO;
}
NSDictionary *properties = [decoder properties];
[decoder close];
decoder = nil;
[source close];
source = nil;
int impulseChannels = [[properties objectForKey:@"channels"] intValue];
if([[properties objectForKey:@"floatingPoint"] boolValue] != YES ||
[[properties objectForKey:@"bitsPerSample"] intValue] != 32 ||
!([[properties objectForKey:@"endian"] isEqualToString:@"host"] ||
[[properties objectForKey:@"endian"] isEqualToString:@"little"]) ||
(impulseChannels != 14 && impulseChannels != 7))
return NO;
return YES;
}
- (id)initWithImpulseFile:(NSURL *)url forSampleRate:(double)sampleRate withInputChannels:(size_t)channels withConfig:(uint32_t)config {
self = [super init];
if(self) {
int sampleCount = 0;
int impulseChannels = 0;
const float *impulseBuffer = [[impulseCache sharedController] getImpulse:url sampleCount:&sampleCount channelCount:&impulseChannels sampleRate:sampleRate];
if(!impulseBuffer) {
return nil;
}
channelCount = channels;
bufferSize = 512;
fftSize = sampleCount + bufferSize;
int pow = 1;
while(fftSize > 2) {
pow++;
fftSize /= 2;
}
fftSize = 2 << pow;
float *deinterleavedImpulseBuffer = (float *)_memalign_malloc(fftSize * sizeof(float) * impulseChannels, 16);
if(!deinterleavedImpulseBuffer) {
return nil;
}
for(size_t i = 0; i < impulseChannels; ++i) {
cblas_scopy(sampleCount, impulseBuffer + i, impulseChannels, deinterleavedImpulseBuffer + i * fftSize, 1);
vDSP_vclr(deinterleavedImpulseBuffer + i * fftSize + sampleCount, 1, fftSize - sampleCount);
}
paddedBufferSize = fftSize;
fftSizeOver2 = (fftSize + 1) / 2;
const size_t fftSizeOver2Plus1 = fftSizeOver2 + 1; // DFT float overwrites plus one, double doesn't
dftSetupF = vDSP_DFT_zrop_CreateSetup(nil, fftSize, vDSP_DFT_FORWARD);
dftSetupB = vDSP_DFT_zrop_CreateSetup(nil, fftSize, vDSP_DFT_INVERSE);
if(!dftSetupF || !dftSetupB) {
free(deinterleavedImpulseBuffer);
return nil;
}
paddedSignal = (float *)_memalign_malloc(sizeof(float) * paddedBufferSize, 16);
if(!paddedSignal) {
free(deinterleavedImpulseBuffer);
return nil;
}
signal_fft.realp = (float *)_memalign_malloc(sizeof(float) * fftSizeOver2Plus1, 16);
signal_fft.imagp = (float *)_memalign_malloc(sizeof(float) * fftSizeOver2Plus1, 16);
if(!signal_fft.realp || !signal_fft.imagp) {
free(deinterleavedImpulseBuffer);
return nil;
}
input_filtered_signal_per_channel[0].realp = (float *)_memalign_malloc(sizeof(float) * fftSizeOver2Plus1, 16);
input_filtered_signal_per_channel[0].imagp = (float *)_memalign_malloc(sizeof(float) * fftSizeOver2Plus1, 16);
if(!input_filtered_signal_per_channel[0].realp ||
!input_filtered_signal_per_channel[0].imagp) {
free(deinterleavedImpulseBuffer);
return nil;
}
input_filtered_signal_per_channel[1].realp = (float *)_memalign_malloc(sizeof(float) * fftSizeOver2Plus1, 16);
input_filtered_signal_per_channel[1].imagp = (float *)_memalign_malloc(sizeof(float) * fftSizeOver2Plus1, 16);
if(!input_filtered_signal_per_channel[1].realp ||
!input_filtered_signal_per_channel[1].imagp) {
free(deinterleavedImpulseBuffer);
return nil;
}
input_filtered_signal_totals[0].realp = (float *)_memalign_malloc(sizeof(float) * fftSizeOver2Plus1, 16);
input_filtered_signal_totals[0].imagp = (float *)_memalign_malloc(sizeof(float) * fftSizeOver2Plus1, 16);
if(!input_filtered_signal_totals[0].realp ||
!input_filtered_signal_totals[0].imagp) {
free(deinterleavedImpulseBuffer);
return nil;
}
input_filtered_signal_totals[1].realp = (float *)_memalign_malloc(sizeof(float) * fftSizeOver2Plus1, 16);
input_filtered_signal_totals[1].imagp = (float *)_memalign_malloc(sizeof(float) * fftSizeOver2Plus1, 16);
if(!input_filtered_signal_totals[1].realp ||
!input_filtered_signal_totals[1].imagp) {
free(deinterleavedImpulseBuffer);
return nil;
}
impulse_responses = (DSPSplitComplex *)calloc(sizeof(DSPSplitComplex), channels * 2);
if(!impulse_responses) {
free(deinterleavedImpulseBuffer);
return nil;
}
for(size_t i = 0; i < channels; ++i) {
impulse_responses[i * 2 + 0].realp = (float *)_memalign_malloc(sizeof(float) * fftSizeOver2Plus1, 16);
impulse_responses[i * 2 + 0].imagp = (float *)_memalign_malloc(sizeof(float) * fftSizeOver2Plus1, 16);
impulse_responses[i * 2 + 1].realp = (float *)_memalign_malloc(sizeof(float) * fftSizeOver2Plus1, 16);
impulse_responses[i * 2 + 1].imagp = (float *)_memalign_malloc(sizeof(float) * fftSizeOver2Plus1, 16);
if(!impulse_responses[i * 2 + 0].realp || !impulse_responses[i * 2 + 0].imagp ||
!impulse_responses[i * 2 + 1].realp || !impulse_responses[i * 2 + 1].imagp) {
free(deinterleavedImpulseBuffer);
return nil;
}
uint32_t channelFlag = [AudioChunk extractChannelFlag:(uint32_t)i fromConfig:config];
uint32_t channelIndex = [AudioChunk findChannelIndex:channelFlag];
int leftInChannel = speaker_not_present;
int rightInChannel = speaker_not_present;
if(impulseChannels == 7) {
if(channelIndex <= max_speaker_index) {
leftInChannel = speakers_to_hesuvi_7[channelIndex][0];
rightInChannel = speakers_to_hesuvi_7[channelIndex][1];
}
} else {
if(channelIndex <= max_speaker_index) {
leftInChannel = speakers_to_hesuvi_14[channelIndex][0];
rightInChannel = speakers_to_hesuvi_14[channelIndex][1];
}
}
if(leftInChannel == speaker_is_back_center || rightInChannel == speaker_is_back_center) {
float *temp;
if(impulseChannels == 7) {
temp = (float *)malloc(sizeof(float) * fftSize);
if(!temp) {
free(deinterleavedImpulseBuffer);
return nil;
}
cblas_scopy((int)fftSize, deinterleavedImpulseBuffer + 4 * fftSize, 1, temp, 1);
vDSP_vadd(temp, 1, deinterleavedImpulseBuffer + 5 * fftSize, 1, temp, 1, fftSize);
vDSP_ctoz((DSPComplex *)temp, 2, &impulse_responses[i * 2 + 0], 1, fftSizeOver2);
vDSP_ctoz((DSPComplex *)temp, 2, &impulse_responses[i * 2 + 1], 1, fftSizeOver2);
} else {
temp = (float *)malloc(sizeof(float) * fftSize * 2);
if(!temp) {
free(deinterleavedImpulseBuffer);
return nil;
}
cblas_scopy((int)fftSize, deinterleavedImpulseBuffer + 4 * fftSize, 1, temp, 1);
vDSP_vadd(temp, 1, deinterleavedImpulseBuffer + 12 * fftSize, 1, temp, 1, fftSize);
cblas_scopy((int)fftSize, deinterleavedImpulseBuffer + 5 * fftSize, 1, temp + fftSize, 1);
vDSP_vadd(temp + fftSize, 1, deinterleavedImpulseBuffer + 11 * fftSize, 1, temp + fftSize, 1, fftSize);
vDSP_ctoz((DSPComplex *)temp, 2, &impulse_responses[i * 2 + 0], 1, fftSizeOver2);
vDSP_ctoz((DSPComplex *)(temp + fftSize), 2, &impulse_responses[i * 2 + 1], 1, fftSizeOver2);
}
free(temp);
} else if(leftInChannel == speaker_not_present || rightInChannel == speaker_not_present) {
vDSP_ctoz((DSPComplex *)(deinterleavedImpulseBuffer + impulseChannels * fftSize), 2, &impulse_responses[i * 2 + 0], 1, fftSizeOver2);
vDSP_ctoz((DSPComplex *)(deinterleavedImpulseBuffer + impulseChannels * fftSize), 2, &impulse_responses[i * 2 + 1], 1, fftSizeOver2);
} else {
vDSP_ctoz((DSPComplex *)(deinterleavedImpulseBuffer + leftInChannel * fftSize), 2, &impulse_responses[i * 2 + 0], 1, fftSizeOver2);
vDSP_ctoz((DSPComplex *)(deinterleavedImpulseBuffer + rightInChannel * fftSize), 2, &impulse_responses[i * 2 + 1], 1, fftSizeOver2);
}
vDSP_DFT_Execute(dftSetupF, impulse_responses[i * 2 + 0].realp, impulse_responses[i * 2 + 0].imagp, impulse_responses[i * 2 + 0].realp, impulse_responses[i * 2 + 0].imagp);
vDSP_DFT_Execute(dftSetupF, impulse_responses[i * 2 + 1].realp, impulse_responses[i * 2 + 1].imagp, impulse_responses[i * 2 + 1].realp, impulse_responses[i * 2 + 1].imagp);
}
free(deinterleavedImpulseBuffer);
left_result = (float *)_memalign_malloc(sizeof(float) * fftSize, 16);
right_result = (float *)_memalign_malloc(sizeof(float) * fftSize, 16);
if(!left_result || !right_result)
return nil;
prevInputs = (float **)calloc(channels, sizeof(float *));
if(!prevInputs)
return nil;
for(size_t i = 0; i < channels; ++i) {
prevInputs[i] = (float *)_memalign_malloc(sizeof(float) * fftSize, 16);
if(!prevInputs[i])
return nil;
vDSP_vclr(prevInputs[i], 1, fftSize);
}
}
return self;
}
- (void)dealloc {
if(dftSetupF) vDSP_DFT_DestroySetup(dftSetupF);
if(dftSetupB) vDSP_DFT_DestroySetup(dftSetupB);
free(paddedSignal);
free(signal_fft.realp);
free(signal_fft.imagp);
free(input_filtered_signal_per_channel[0].realp);
free(input_filtered_signal_per_channel[0].imagp);
free(input_filtered_signal_per_channel[1].realp);
free(input_filtered_signal_per_channel[1].imagp);
free(input_filtered_signal_totals[0].realp);
free(input_filtered_signal_totals[0].imagp);
free(input_filtered_signal_totals[1].realp);
free(input_filtered_signal_totals[1].imagp);
if(impulse_responses) {
for(size_t i = 0; i < channelCount * 2; ++i) {
free(impulse_responses[i].realp);
free(impulse_responses[i].imagp);
}
free(impulse_responses);
}
free(left_result);
free(right_result);
if(prevInputs) {
for(size_t i = 0; i < channelCount; ++i) {
free(prevInputs[i]);
}
free(prevInputs);
}
}
- (void)process:(const float *)inBuffer sampleCount:(size_t)count toBuffer:(float *)outBuffer {
const float scale = 1.0 / (4.0 * (float)fftSize);
while(count > 0) {
const size_t countToDo = (count > bufferSize) ? bufferSize : count;
const size_t prevToDo = fftSize - countToDo;
vDSP_vclr(input_filtered_signal_totals[0].realp, 1, fftSizeOver2);
vDSP_vclr(input_filtered_signal_totals[0].imagp, 1, fftSizeOver2);
vDSP_vclr(input_filtered_signal_totals[1].realp, 1, fftSizeOver2);
vDSP_vclr(input_filtered_signal_totals[1].imagp, 1, fftSizeOver2);
for(size_t i = 0; i < channelCount; ++i) {
cblas_scopy((int)prevToDo, prevInputs[i] + countToDo, 1, paddedSignal, 1);
cblas_scopy((int)countToDo, inBuffer + i, (int)channelCount, paddedSignal + prevToDo, 1);
cblas_scopy((int)fftSize, paddedSignal, 1, prevInputs[i], 1);
vDSP_ctoz((DSPComplex *)paddedSignal, 2, &signal_fft, 1, fftSizeOver2);
vDSP_DFT_Execute(dftSetupF, signal_fft.realp, signal_fft.imagp, signal_fft.realp, signal_fft.imagp);
// One channel forward, then multiply and back twice
float preserveIRNyq = impulse_responses[i * 2 + 0].imagp[0];
float preserveSigNyq = signal_fft.imagp[0];
impulse_responses[i * 2 + 0].imagp[0] = 0;
signal_fft.imagp[0] = 0;
vDSP_zvmul(&signal_fft, 1, &impulse_responses[i * 2 + 0], 1, &input_filtered_signal_per_channel[0], 1, fftSizeOver2, 1);
input_filtered_signal_per_channel[0].imagp[0] = preserveIRNyq * preserveSigNyq;
impulse_responses[i * 2 + 0].imagp[0] = preserveIRNyq;
preserveIRNyq = impulse_responses[i * 2 + 1].imagp[0];
impulse_responses[i * 2 + 1].imagp[0] = 0;
vDSP_zvmul(&signal_fft, 1, &impulse_responses[i * 2 + 1], 1, &input_filtered_signal_per_channel[1], 1, fftSizeOver2, 1);
input_filtered_signal_per_channel[1].imagp[0] = preserveIRNyq * preserveSigNyq;
impulse_responses[i * 2 + 1].imagp[0] = preserveIRNyq;
vDSP_zvadd(&input_filtered_signal_totals[0], 1, &input_filtered_signal_per_channel[0], 1, &input_filtered_signal_totals[0], 1, fftSizeOver2);
vDSP_zvadd(&input_filtered_signal_totals[1], 1, &input_filtered_signal_per_channel[1], 1, &input_filtered_signal_totals[1], 1, fftSizeOver2);
}
vDSP_DFT_Execute(dftSetupB, input_filtered_signal_totals[0].realp, input_filtered_signal_totals[0].imagp, input_filtered_signal_totals[0].realp, input_filtered_signal_totals[0].imagp);
vDSP_DFT_Execute(dftSetupB, input_filtered_signal_totals[1].realp, input_filtered_signal_totals[1].imagp, input_filtered_signal_totals[1].realp, input_filtered_signal_totals[1].imagp);
vDSP_ztoc(&input_filtered_signal_totals[0], 1, (DSPComplex *)left_result, 2, fftSizeOver2);
vDSP_ztoc(&input_filtered_signal_totals[1], 1, (DSPComplex *)right_result, 2, fftSizeOver2);
float *left_ptr = left_result + prevToDo;
float *right_ptr = right_result + prevToDo;
vDSP_vsmul(left_ptr, 1, &scale, left_ptr, 1, countToDo);
vDSP_vsmul(right_ptr, 1, &scale, right_ptr, 1, countToDo);
cblas_scopy((int)countToDo, left_ptr, 1, outBuffer + 0, 2);
cblas_scopy((int)countToDo, right_ptr, 1, outBuffer + 1, 2);
inBuffer += countToDo * channelCount;
outBuffer += countToDo * 2;
count -= countToDo;
}
}
- (void)reset {
for(size_t i = 0; i < channelCount; ++i) {
vDSP_vclr(prevInputs[i], 1, fftSize);
}
}
@end

View file

@ -1,45 +0,0 @@
//
// HeadphoneFilter.h
// CogAudio Framework
//
// Created by Christopher Snowhill on 1/24/22.
//
#ifndef HeadphoneFilter_h
#define HeadphoneFilter_h
#import <Cocoa/Cocoa.h>
#import "pffft.h"
@interface HeadphoneFilter : NSObject {
PFFFT_Setup *fftSetup;
size_t fftSize;
size_t bufferSize;
size_t paddedBufferSize;
size_t channelCount;
float *workBuffer;
float **impulse_responses;
float **prevInputs;
float *left_result;
float *right_result;
float *paddedSignal;
}
+ (BOOL)validateImpulseFile:(NSURL *)url;
- (id)initWithImpulseFile:(NSURL *)url forSampleRate:(double)sampleRate withInputChannels:(size_t)channels withConfig:(uint32_t)config;
- (void)process:(const float *)inBuffer sampleCount:(size_t)count toBuffer:(float *)outBuffer;
- (void)reset;
@end
#endif /* HeadphoneFilter_h */

View file

@ -1,447 +0,0 @@
//
// HeadphoneFilter.m
// CogAudio Framework
//
// Created by Christopher Snowhill on 1/24/22.
//
#import "AudioChunk.h"
#import "AudioDecoder.h"
#import "AudioSource.h"
#import "HeadphoneFilter.h"
#import <stdlib.h>
#import "r8bstate.h"
#import "lpc.h"
#import "util.h"
#import "pffft_double.h"
@implementation HeadphoneFilter
enum {
speaker_is_back_center = -1,
speaker_not_present = -2,
};
static const uint32_t max_speaker_index = 10;
static const int8_t speakers_to_hesuvi_7[11][2] = {
// front left
{ 0, 1 },
// front right
{ 1, 0 },
// front center
{ 6, 6 },
// lfe
{ 6, 6 },
// back left
{ 4, 5 },
// back right
{ 5, 4 },
// front center left
{ speaker_not_present, speaker_not_present },
// front center right
{ speaker_not_present, speaker_not_present },
// back center
{ speaker_is_back_center, speaker_is_back_center },
// side left
{ 2, 3 },
// side right
{ 3, 2 }
};
static const int8_t speakers_to_hesuvi_14[11][2] = {
// front left
{ 0, 1 },
// front right
{ 8, 7 },
// front center
{ 6, 13 },
// lfe
{ 6, 13 },
// back left
{ 4, 5 },
// back right
{ 12, 11 },
// front center left
{ speaker_not_present, speaker_not_present },
// front center right
{ speaker_not_present, speaker_not_present },
// back center
{ speaker_is_back_center, speaker_is_back_center },
// side left
{ 2, 3 },
// side right
{ 10, 9 }
};
+ (BOOL)validateImpulseFile:(NSURL *)url {
id<CogSource> source = [AudioSource audioSourceForURL:url];
if(!source)
return NO;
if(![source open:url])
return NO;
id<CogDecoder> decoder = [AudioDecoder audioDecoderForSource:source];
if(decoder == nil) {
[source close];
source = nil;
return NO;
}
if(![decoder open:source]) {
decoder = nil;
[source close];
source = nil;
return NO;
}
NSDictionary *properties = [decoder properties];
[decoder close];
decoder = nil;
[source close];
source = nil;
int impulseChannels = [[properties objectForKey:@"channels"] intValue];
if([[properties objectForKey:@"floatingPoint"] boolValue] != YES ||
[[properties objectForKey:@"bitsPerSample"] intValue] != 32 ||
!([[properties objectForKey:@"endian"] isEqualToString:@"host"] ||
[[properties objectForKey:@"endian"] isEqualToString:@"little"]) ||
(impulseChannels != 14 && impulseChannels != 7))
return NO;
return YES;
}
- (id)initWithImpulseFile:(NSURL *)url forSampleRate:(double)sampleRate withInputChannels:(size_t)channels withConfig:(uint32_t)config {
self = [super init];
if(self) {
id<CogSource> source = [AudioSource audioSourceForURL:url];
if(!source)
return nil;
if(![source open:url])
return nil;
id<CogDecoder> decoder = [AudioDecoder audioDecoderForSource:source];
if(decoder == nil) {
[source close];
source = nil;
return nil;
}
if(![decoder open:source]) {
decoder = nil;
[source close];
source = nil;
return nil;
}
NSDictionary *properties = [decoder properties];
double sampleRateOfSource = [[properties objectForKey:@"sampleRate"] doubleValue];
int sampleCount = [[properties objectForKey:@"totalFrames"] intValue];
int impulseChannels = [[properties objectForKey:@"channels"] intValue];
if([[properties objectForKey:@"floatingPoint"] boolValue] != YES ||
[[properties objectForKey:@"bitsPerSample"] intValue] != 32 ||
!([[properties objectForKey:@"endian"] isEqualToString:@"host"] ||
[[properties objectForKey:@"endian"] isEqualToString:@"little"]) ||
(impulseChannels != 14 && impulseChannels != 7)) {
[decoder close];
decoder = nil;
[source close];
source = nil;
return nil;
}
float *impulseBuffer = (float *)pffft_aligned_malloc(sampleCount * sizeof(float) * impulseChannels);
if(!impulseBuffer) {
[decoder close];
decoder = nil;
[source close];
source = nil;
return nil;
}
if([decoder readAudio:impulseBuffer frames:sampleCount] != sampleCount) {
pffft_aligned_free(impulseBuffer);
[decoder close];
decoder = nil;
[source close];
source = nil;
return nil;
}
[decoder close];
decoder = nil;
[source close];
source = nil;
if(sampleRateOfSource != sampleRate) {
double sampleRatio = sampleRate / sampleRateOfSource;
int resampledCount = (int)ceil((double)sampleCount * sampleRatio);
r8bstate *_r8bstate = new r8bstate(impulseChannels, 1024, sampleRateOfSource, sampleRate);
unsigned long PRIME_LEN_ = MAX(sampleRateOfSource / 20, 1024u);
PRIME_LEN_ = MIN(PRIME_LEN_, 16384u);
PRIME_LEN_ = MAX(PRIME_LEN_, 2 * LPC_ORDER + 1);
unsigned int N_samples_to_add_ = sampleRateOfSource;
unsigned int N_samples_to_drop_ = sampleRate;
samples_len(&N_samples_to_add_, &N_samples_to_drop_, 20, 8192u);
int resamplerLatencyIn = (int)N_samples_to_add_;
int resamplerLatencyOut = (int)N_samples_to_drop_;
float *tempImpulse = (float *)pffft_aligned_malloc((sampleCount + resamplerLatencyIn * 2 + 1024) * sizeof(float) * impulseChannels);
if(!tempImpulse) {
pffft_aligned_free(impulseBuffer);
return nil;
}
resampledCount += resamplerLatencyOut * 2 + 1024;
float *resampledImpulse = (float *)pffft_aligned_malloc(resampledCount * sizeof(float) * impulseChannels);
if(!resampledImpulse) {
pffft_aligned_free(impulseBuffer);
pffft_aligned_free(tempImpulse);
return nil;
}
size_t prime = MIN(sampleCount, PRIME_LEN_);
void *extrapolate_buffer = NULL;
size_t extrapolate_buffer_size = 0;
memcpy(tempImpulse + resamplerLatencyIn * impulseChannels, impulseBuffer, sampleCount * sizeof(float) * impulseChannels);
lpc_extrapolate_bkwd(tempImpulse + N_samples_to_add_ * impulseChannels, sampleCount, prime, impulseChannels, LPC_ORDER, N_samples_to_add_, &extrapolate_buffer, &extrapolate_buffer_size);
lpc_extrapolate_fwd(tempImpulse + N_samples_to_add_ * impulseChannels, sampleCount, prime, impulseChannels, LPC_ORDER, N_samples_to_add_, &extrapolate_buffer, &extrapolate_buffer_size);
free(extrapolate_buffer);
size_t inputDone = 0;
size_t outputDone = 0;
outputDone = _r8bstate->resample(tempImpulse, sampleCount + N_samples_to_add_ * 2, &inputDone, resampledImpulse, resampledCount);
if(outputDone < resampledCount) {
outputDone += _r8bstate->flush(resampledImpulse + outputDone * impulseChannels, resampledCount - outputDone);
}
delete _r8bstate;
outputDone -= N_samples_to_drop_ * 2;
memmove(resampledImpulse, resampledImpulse + N_samples_to_drop_ * impulseChannels, outputDone * sizeof(float) * impulseChannels);
pffft_aligned_free(tempImpulse);
pffft_aligned_free(impulseBuffer);
impulseBuffer = resampledImpulse;
sampleCount = (int)outputDone;
// Normalize resampled impulse by sample ratio
float fSampleRatio = (float)sampleRatio;
vDSP_vsdiv(impulseBuffer, 1, &fSampleRatio, impulseBuffer, 1, sampleCount * impulseChannels);
}
channelCount = channels;
bufferSize = 512;
fftSize = sampleCount + bufferSize;
fftSize = (size_t)pffftd_next_power_of_two((int)fftSize);
float *deinterleavedImpulseBuffer = (float *)pffft_aligned_malloc(fftSize * sizeof(float) * impulseChannels);
if(!deinterleavedImpulseBuffer) {
pffft_aligned_free(impulseBuffer);
return nil;
}
for(size_t i = 0; i < impulseChannels; ++i) {
cblas_scopy(sampleCount, impulseBuffer + i, impulseChannels, deinterleavedImpulseBuffer + i * fftSize, 1);
vDSP_vclr(deinterleavedImpulseBuffer + i * fftSize + sampleCount, 1, fftSize - sampleCount);
}
pffft_aligned_free(impulseBuffer);
paddedBufferSize = fftSize;
fftSetup = pffft_new_setup((int)fftSize, PFFFT_REAL);
if(!fftSetup) {
pffft_aligned_free(deinterleavedImpulseBuffer);
return nil;
}
workBuffer = (float *)pffft_aligned_malloc(sizeof(float) * fftSize);
if(!workBuffer) {
pffft_aligned_free(deinterleavedImpulseBuffer);
return nil;
}
paddedSignal = (float *)pffft_aligned_malloc(sizeof(float) * paddedBufferSize);
if(!paddedSignal) {
pffft_aligned_free(deinterleavedImpulseBuffer);
return nil;
}
impulse_responses = (float **)calloc(sizeof(float *), channels * 2);
if(!impulse_responses) {
pffft_aligned_free(deinterleavedImpulseBuffer);
return nil;
}
for(size_t i = 0; i < channels; ++i) {
impulse_responses[i * 2 + 0] = (float *)pffft_aligned_malloc(sizeof(float) * fftSize * 2);
impulse_responses[i * 2 + 1] = (float *)pffft_aligned_malloc(sizeof(float) * fftSize * 2);
if(!impulse_responses[i * 2 + 0] || !impulse_responses[i * 2 + 1]) {
pffft_aligned_free(deinterleavedImpulseBuffer);
return nil;
}
uint32_t channelFlag = [AudioChunk extractChannelFlag:(uint32_t)i fromConfig:config];
uint32_t channelIndex = [AudioChunk findChannelIndex:channelFlag];
int leftInChannel = speaker_not_present;
int rightInChannel = speaker_not_present;
if(impulseChannels == 7) {
if(channelIndex <= max_speaker_index) {
leftInChannel = speakers_to_hesuvi_7[channelIndex][0];
rightInChannel = speakers_to_hesuvi_7[channelIndex][1];
}
} else {
if(channelIndex <= max_speaker_index) {
leftInChannel = speakers_to_hesuvi_14[channelIndex][0];
rightInChannel = speakers_to_hesuvi_14[channelIndex][1];
}
}
if(leftInChannel == speaker_is_back_center || rightInChannel == speaker_is_back_center) {
if(impulseChannels == 7) {
cblas_scopy((int)fftSize, deinterleavedImpulseBuffer + 4 * fftSize, 1, impulse_responses[i * 2 + 0], 1);
vDSP_vadd(impulse_responses[i * 2 + 0], 1, deinterleavedImpulseBuffer + 5 * fftSize, 1, impulse_responses[i * 2 + 0], 1, fftSize);
cblas_scopy((int)fftSize, impulse_responses[i * 2 + 0], 1, impulse_responses[i * 2 + 1], 1);
} else {
cblas_scopy((int)fftSize, deinterleavedImpulseBuffer + 4 * fftSize, 1, impulse_responses[i * 2 + 0], 1);
vDSP_vadd(impulse_responses[i * 2 + 0], 1, deinterleavedImpulseBuffer + 12 * fftSize, 1, impulse_responses[i * 2 + 0], 1, fftSize);
cblas_scopy((int)fftSize, deinterleavedImpulseBuffer + 5 * fftSize, 1, impulse_responses[i * 2 + 1], 1);
vDSP_vadd(impulse_responses[i * 2 + 1], 1, deinterleavedImpulseBuffer + 11 * fftSize, 1, impulse_responses[i * 2 + 1], 1, fftSize);
}
} else if(leftInChannel == speaker_not_present || rightInChannel == speaker_not_present) {
vDSP_vclr(impulse_responses[i * 2 + 0], 1, fftSize);
vDSP_vclr(impulse_responses[i * 2 + 1], 1, fftSize);
} else {
cblas_scopy((int)fftSize, deinterleavedImpulseBuffer + leftInChannel * fftSize, 1, impulse_responses[i * 2 + 0], 1);
cblas_scopy((int)fftSize, deinterleavedImpulseBuffer + rightInChannel * fftSize, 1, impulse_responses[i * 2 + 1], 1);
}
pffft_transform(fftSetup, impulse_responses[i * 2 + 0], impulse_responses[i * 2 + 0], workBuffer, PFFFT_FORWARD);
pffft_transform(fftSetup, impulse_responses[i * 2 + 1], impulse_responses[i * 2 + 1], workBuffer, PFFFT_FORWARD);
}
pffft_aligned_free(deinterleavedImpulseBuffer);
left_result = (float *)pffft_aligned_malloc(sizeof(float) * fftSize);
right_result = (float *)pffft_aligned_malloc(sizeof(float) * fftSize);
if(!left_result || !right_result)
return nil;
prevInputs = (float **)calloc(sizeof(float *), channels);
if(!prevInputs) {
return nil;
}
for(size_t i = 0; i < channels; ++i) {
prevInputs[i] = (float *)pffft_aligned_malloc(sizeof(float) * fftSize);
if(!prevInputs[i]) {
return nil;
}
vDSP_vclr(prevInputs[i], 1, fftSize);
}
}
return self;
}
- (void)dealloc {
if(fftSetup) pffft_destroy_setup(fftSetup);
pffft_aligned_free(workBuffer);
pffft_aligned_free(paddedSignal);
if(impulse_responses) {
for(size_t i = 0; i < channelCount * 2; ++i) {
pffft_aligned_free(impulse_responses[i]);
}
free(impulse_responses);
}
if(prevInputs) {
for(size_t i = 0; i < channelCount; ++i) {
pffft_aligned_free(prevInputs[i]);
}
free(prevInputs);
}
pffft_aligned_free(left_result);
pffft_aligned_free(right_result);
}
- (void)process:(const float *)inBuffer sampleCount:(size_t)count toBuffer:(float *)outBuffer {
const float scale = 1.0 / ((float)fftSize);
while(count > 0) {
const size_t countToDo = (count > bufferSize) ? bufferSize : count;
const size_t outOffset = fftSize - countToDo;
vDSP_vclr(left_result, 1, fftSize);
vDSP_vclr(right_result, 1, fftSize);
for(size_t i = 0; i < channelCount; ++i) {
cblas_scopy((int)outOffset, prevInputs[i] + countToDo, 1, paddedSignal, 1);
cblas_scopy((int)countToDo, inBuffer + i, (int)channelCount, paddedSignal + outOffset, 1);
cblas_scopy((int)fftSize, paddedSignal, 1, prevInputs[i], 1);
pffft_transform(fftSetup, paddedSignal, paddedSignal, workBuffer, PFFFT_FORWARD);
pffft_zconvolve_accumulate(fftSetup, paddedSignal, impulse_responses[i * 2 + 0], left_result, 1.0);
pffft_zconvolve_accumulate(fftSetup, paddedSignal, impulse_responses[i * 2 + 1], right_result, 1.0);
}
pffft_transform(fftSetup, left_result, left_result, workBuffer, PFFFT_BACKWARD);
pffft_transform(fftSetup, right_result, right_result, workBuffer, PFFFT_BACKWARD);
vDSP_vsmul(left_result + outOffset, 1, &scale, left_result + outOffset, 1, countToDo);
vDSP_vsmul(right_result + outOffset, 1, &scale, right_result + outOffset, 1, countToDo);
cblas_scopy((int)countToDo, left_result + outOffset, 1, outBuffer + 0, 2);
cblas_scopy((int)countToDo, right_result + outOffset, 1, outBuffer + 1, 2);
inBuffer += countToDo * channelCount;
outBuffer += countToDo * 2;
count -= countToDo;
}
}
- (void)reset {
for(size_t i = 0; i < channelCount; ++i) {
vDSP_vclr(prevInputs[i], 1, fftSize);
}
}
@end

View file

@ -12,9 +12,9 @@
#import <AudioUnit/AudioUnit.h>
#import <CoreAudio/AudioHardware.h>
#import "AudioDecoder.h"
#import "Node.h"
#import "Plugin.h"
#import <CogAudio/AudioDecoder.h>
#import <CogAudio/Node.h>
#import <CogAudio/Plugin.h>
#define INPUT_NODE_SEEK
@ -33,19 +33,22 @@
Semaphore *exitAtTheEndOfTheStream;
}
@property(readonly) Semaphore *exitAtTheEndOfTheStream;
@property(readonly) Semaphore * _Nonnull exitAtTheEndOfTheStream;
@property(readonly) BOOL threadExited;
- (BOOL)openWithSource:(id<CogSource>)source;
- (BOOL)openWithDecoder:(id<CogDecoder>)d;
- (id _Nullable)initWithController:(id _Nonnull)c previous:(id _Nullable)p;
- (BOOL)openWithSource:(id<CogSource>_Nonnull)source;
- (BOOL)openWithDecoder:(id<CogDecoder>_Nonnull)d;
- (void)process;
- (NSDictionary *)properties;
- (NSDictionary *_Nonnull)properties;
- (void)seek:(long)frame;
- (void)registerObservers;
- (BOOL)setTrack:(NSURL *)track;
- (BOOL)setTrack:(NSURL *_Nonnull)track;
- (id<CogDecoder>)decoder;
- (id<CogDecoder>_Nonnull)decoder;
@end

View file

@ -21,12 +21,14 @@
static void *kInputNodeContext = &kInputNodeContext;
@synthesize threadExited;
@synthesize exitAtTheEndOfTheStream;
- (id)initWithController:(id)c previous:(id)p {
self = [super initWithController:c previous:p];
if(self) {
exitAtTheEndOfTheStream = [[Semaphore alloc] init];
threadExited = NO;
}
return self;
@ -142,10 +144,6 @@ static void *kInputNodeContext = &kInputNodeContext;
}
- (void)process {
int amountInBuffer = 0;
int bytesInBuffer = 0;
void *inputBuffer = malloc(CHUNK_SIZE * 8 * 18); // Maximum 18 channels, dunno what we'll receive
BOOL shouldClose = YES;
BOOL seekError = NO;
@ -161,20 +159,19 @@ static void *kInputNodeContext = &kInputNodeContext;
while([self shouldContinue] == YES && [self endOfStream] == NO) {
if(shouldSeek == YES) {
BufferChain *bufferChain = [[controller controller] bufferChain];
ConverterNode *converter = [bufferChain converter];
DLog(@"SEEKING! Resetting Buffer");
BufferChain *bufferChain = controller;
AudioPlayer *audioPlayer = [bufferChain controller];
OutputNode *outputNode = [audioPlayer output];
amountInBuffer = 0;
// This resets the converter's buffer
[self resetBuffer];
[converter resetBuffer];
[converter inputFormatDidChange:[bufferChain inputFormat] inputConfig:[bufferChain inputConfig]];
DLog(@"SEEKING! Resetting Buffer");
[outputNode resetBackwards];
DLog(@"Reset buffer!");
DLog(@"SEEKING!");
seekError = [decoder seek:seekFrame] < 0;
@autoreleasepool {
seekError = [decoder seek:seekFrame] < 0;
}
shouldSeek = NO;
DLog(@"Seeked! Resetting Buffer");
@ -185,43 +182,44 @@ static void *kInputNodeContext = &kInputNodeContext;
}
}
if(amountInBuffer < CHUNK_SIZE) {
int framesToRead = CHUNK_SIZE - amountInBuffer;
int framesRead;
AudioChunk *chunk;
@autoreleasepool {
chunk = [decoder readAudio];
}
if(chunk && [chunk frameCount]) {
@autoreleasepool {
framesRead = [decoder readAudio:((char *)inputBuffer) + bytesInBuffer frames:framesToRead];
[self writeChunk:chunk];
chunk = nil;
}
} else {
if(chunk) {
@autoreleasepool {
chunk = nil;
}
}
DLog(@"End of stream? %@", [self properties]);
endOfStream = YES;
shouldClose = [controller endOfInputReached]; // Lets us know if we should keep going or not (occassionally, for track changes within a file)
DLog(@"closing? is %i", shouldClose);
// Move this here, so that the above endOfInputReached has a chance to queue another track before starting output
// Technically, the output should still play out its buffer first before checking if it should stop
if(initialBufferFilled == NO) {
[controller initialBufferFilled:self];
}
if(framesRead > 0 && !seekError) {
amountInBuffer += framesRead;
bytesInBuffer += framesRead * bytesPerFrame;
[self writeData:inputBuffer amount:bytesInBuffer];
amountInBuffer = 0;
bytesInBuffer = 0;
// wait before exiting, as we might still get seeking request
DLog("InputNode: Before wait")
[exitAtTheEndOfTheStream waitIndefinitely];
DLog("InputNode: After wait, should seek = %d", shouldSeek);
if(shouldSeek) {
endOfStream = NO;
shouldClose = NO;
continue;
} else {
DLog(@"End of stream? %@", [self properties]);
endOfStream = YES;
shouldClose = [controller endOfInputReached]; // Lets us know if we should keep going or not (occassionally, for track changes within a file)
DLog(@"closing? is %i", shouldClose);
// Move this here, so that the above endOfInputReached has a chance to queue another track before starting output
// Technically, the output should still play out its buffer first before checking if it should stop
if(initialBufferFilled == NO) {
[controller initialBufferFilled:self];
}
// wait before exiting, as we might still get seeking request
DLog("InputNode: Before wait")
[exitAtTheEndOfTheStream waitIndefinitely];
DLog("InputNode: After wait, should seek = %d", shouldSeek) if(shouldSeek) {
endOfStream = NO;
shouldClose = NO;
continue;
}
else {
break;
}
break;
}
}
}
@ -229,9 +227,8 @@ static void *kInputNodeContext = &kInputNodeContext;
if(shouldClose)
[decoder close];
free(inputBuffer);
[exitAtTheEndOfTheStream signal];
threadExited = YES;
DLog("Input node thread stopping");
}
@ -240,7 +237,8 @@ static void *kInputNodeContext = &kInputNodeContext;
seekFrame = frame;
shouldSeek = YES;
DLog(@"Should seek!");
[semaphore signal];
[self resetBuffer];
[writeSemaphore signal];
if(endOfStream) {
[exitAtTheEndOfTheStream signal];
@ -274,6 +272,7 @@ static void *kInputNodeContext = &kInputNodeContext;
- (void)dealloc {
DLog(@"Input Node dealloc");
[self removeObservers];
[super cleanUp];
}
- (NSDictionary *)properties {

View file

@ -6,8 +6,8 @@
// Copyright 2006 Vincent Spader. All rights reserved.
//
#import "ChunkList.h"
#import "Semaphore.h"
#import <CogAudio/ChunkList.h>
#import <CogAudio/CogSemaphore.h>
#import <Cocoa/Cocoa.h>
#import <os/workgroup.h>
@ -15,17 +15,25 @@
#define BUFFER_SIZE 1024 * 1024
#define CHUNK_SIZE 16 * 1024
//#define LOG_CHAINS 1
@interface Node : NSObject {
ChunkList *buffer;
Semaphore *semaphore;
Semaphore *writeSemaphore;
Semaphore *readSemaphore;
NSRecursiveLock *accessLock;
NSLock *accessLock;
id __weak previousNode;
id __weak controller;
BOOL shouldReset;
BOOL inWrite;
BOOL inPeek;
BOOL inRead;
BOOL inMerge;
BOOL shouldContinue;
BOOL endOfStream; // All data is now in buffer
BOOL initialBufferFilled;
@ -33,13 +41,34 @@
AudioStreamBasicDescription nodeFormat;
uint32_t nodeChannelConfig;
BOOL nodeLossless;
double durationPrebuffer;
#ifdef LOG_CHAINS
NSFileHandle *logFileOut;
NSFileHandle *logFileIn;
#endif
}
- (id _Nullable)initWithController:(id _Nonnull)c previous:(id _Nullable)p;
#ifdef LOG_CHAINS
- (void)initLogFiles;
#endif
- (void)cleanUp;
- (BOOL)paused;
- (void)writeData:(const void *_Nonnull)ptr amount:(size_t)a;
- (void)writeChunk:(AudioChunk *_Nonnull)chunk;
- (AudioChunk *_Nonnull)readChunk:(size_t)maxFrames;
- (AudioChunk *_Nonnull)readChunkAsFloat32:(size_t)maxFrames;
- (AudioChunk *_Nonnull)readAndMergeChunks:(size_t)maxFrames;
- (AudioChunk *_Nonnull)readAndMergeChunksAsFloat32:(size_t)maxFrames;
- (BOOL)peekFormat:(AudioStreamBasicDescription *_Nonnull)format channelConfig:(uint32_t *_Nonnull)config;
- (BOOL)peekTimestamp:(double *_Nonnull)timestamp timeRatio:(double *_Nonnull)timeRatio;
- (void)process; // Should be overwriten by subclass
- (void)threadEntry:(id _Nullable)arg;
@ -48,6 +77,7 @@
- (void)setShouldReset:(BOOL)s;
- (BOOL)shouldReset;
- (void)resetBackwards;
- (void)setPreviousNode:(id _Nullable)p;
- (id _Nullable)previousNode;
@ -62,7 +92,8 @@
- (uint32_t)nodeChannelConfig;
- (BOOL)nodeLossless;
- (Semaphore *_Nonnull)semaphore;
- (Semaphore *_Nonnull)writeSemaphore;
- (Semaphore *_Nonnull)readSemaphore;
//-(void)resetBuffer;

View file

@ -17,15 +17,41 @@
#import <mach/mach_time.h>
#ifdef LOG_CHAINS
#import "NSFileHandle+CreateFile.h"
static NSLock * _Node_lock = nil;
static uint64_t _Node_serial;
#endif
@implementation Node
#ifdef LOG_CHAINS
+ (void)initialize {
@synchronized (_Node_lock) {
if(!_Node_lock) {
_Node_lock = [[NSLock alloc] init];
_Node_serial = 0;
}
}
}
- (void)initLogFiles {
[_Node_lock lock];
logFileOut = [NSFileHandle fileHandleForWritingAtPath:[NSTemporaryDirectory() stringByAppendingPathComponent:[NSString stringWithFormat:@"%@_output_%08lld.raw", [self className], _Node_serial++]] createFile:YES];
logFileIn = [NSFileHandle fileHandleForWritingAtPath:[NSTemporaryDirectory() stringByAppendingPathComponent:[NSString stringWithFormat:@"%@_input_%08lld.raw", [self className], _Node_serial++]] createFile:YES];
[_Node_lock unlock];
}
#endif
- (id)initWithController:(id)c previous:(id)p {
self = [super init];
if(self) {
buffer = [[ChunkList alloc] initWithMaximumDuration:3.0];
semaphore = [[Semaphore alloc] init];
buffer = [[ChunkList alloc] initWithMaximumDuration:10.0];
writeSemaphore = [[Semaphore alloc] init];
readSemaphore = [[Semaphore alloc] init];
accessLock = [[NSRecursiveLock alloc] init];
accessLock = [[NSLock alloc] init];
initialBufferFilled = NO;
@ -36,12 +62,38 @@
nodeChannelConfig = 0;
nodeLossless = NO;
durationPrebuffer = 2.0;
inWrite = NO;
inPeek = NO;
inRead = NO;
inMerge = NO;
[self setPreviousNode:p];
#ifdef LOG_CHAINS
[self initLogFiles];
#endif
}
return self;
}
- (void)dealloc {
[self cleanUp];
}
- (void)cleanUp {
[self setShouldContinue:NO];
while(inWrite || inPeek || inRead || inMerge) {
[writeSemaphore signal];
if(previousNode) {
[[previousNode readSemaphore] signal];
}
usleep(500);
}
}
- (AudioStreamBasicDescription)nodeFormat {
return nodeFormat;
}
@ -55,6 +107,12 @@
}
- (void)writeData:(const void *)ptr amount:(size_t)amount {
inWrite = YES;
if(!shouldContinue || [self paused]) {
inWrite = NO;
return;
}
[accessLock lock];
AudioChunk *chunk = [[AudioChunk alloc] init];
@ -65,30 +123,104 @@
[chunk setLossless:nodeLossless];
[chunk assignSamples:ptr frameCount:amount / nodeFormat.mBytesPerPacket];
const double chunkDuration = [chunk duration];
double durationLeft = [buffer maxDuration] - [buffer listDuration];
#ifdef LOG_CHAINS
if(logFileOut) {
[logFileOut writeData:[NSData dataWithBytes:ptr length:amount]];
}
#endif
while(shouldContinue == YES && chunkDuration > durationLeft) {
if(durationLeft < chunkDuration) {
if(initialBufferFilled == NO) {
initialBufferFilled = YES;
if([controller respondsToSelector:@selector(initialBufferFilled:)])
[controller performSelector:@selector(initialBufferFilled:) withObject:self];
}
double durationList = [buffer listDuration];
double durationLeft = [buffer maxDuration] - durationList;
if(shouldContinue == YES && durationList >= durationPrebuffer) {
if(initialBufferFilled == NO) {
initialBufferFilled = YES;
if([controller respondsToSelector:@selector(initialBufferFilled:)])
[controller performSelector:@selector(initialBufferFilled:) withObject:self];
}
}
if(durationLeft < chunkDuration || shouldReset) {
while(shouldContinue == YES && ![self paused] && durationLeft < 0.0) {
if(durationLeft < 0.0 || shouldReset) {
[accessLock unlock];
[semaphore wait];
[writeSemaphore timedWait:2000];
[accessLock lock];
}
durationLeft = [buffer maxDuration] - [buffer listDuration];
}
[buffer addChunk:chunk];
BOOL doSignal = NO;
if([chunk frameCount]) {
[buffer addChunk:chunk];
doSignal = YES;
}
[accessLock unlock];
if(doSignal) {
[readSemaphore signal];
}
inWrite = NO;
}
- (void)writeChunk:(AudioChunk *)chunk {
inWrite = YES;
if(!shouldContinue || [self paused]) {
inWrite = NO;
return;
}
[accessLock lock];
double durationList = [buffer listDuration];
double durationLeft = [buffer maxDuration] - durationList;
if(shouldContinue == YES && durationList >= durationPrebuffer) {
if(initialBufferFilled == NO) {
initialBufferFilled = YES;
if([controller respondsToSelector:@selector(initialBufferFilled:)])
[controller performSelector:@selector(initialBufferFilled:) withObject:self];
}
}
while(shouldContinue == YES && ![self paused] && durationLeft < 0.0) {
if(previousNode && [previousNode shouldContinue] == NO) {
shouldContinue = NO;
break;
}
if(durationLeft < 0.0 || shouldReset) {
[accessLock unlock];
[writeSemaphore timedWait:2000];
[accessLock lock];
}
durationLeft = [buffer maxDuration] - [buffer listDuration];
}
BOOL doSignal = NO;
if([chunk frameCount]) {
#ifdef LOG_CHAINS
if(logFileOut) {
AudioChunk *chunkCopy = [chunk copy];
size_t frameCount = [chunkCopy frameCount];
NSData *chunkData = [chunkCopy removeSamples:frameCount];
[logFileOut writeData:chunkData];
}
#endif
[buffer addChunk:chunk];
doSignal = YES;
}
[accessLock unlock];
if(doSignal) {
[readSemaphore signal];
}
inWrite = NO;
}
// Should be overwriten by subclass.
@ -102,21 +234,110 @@
}
- (BOOL)peekFormat:(nonnull AudioStreamBasicDescription *)format channelConfig:(nonnull uint32_t *)config {
inPeek = YES;
if(!shouldContinue || [self paused]) {
inPeek = NO;
return NO;
}
[accessLock lock];
while(shouldContinue && ![self paused] &&
[[previousNode buffer] isEmpty] && [previousNode endOfStream] == NO) {
[accessLock unlock];
[writeSemaphore signal];
[[previousNode readSemaphore] timedWait:2000];
[accessLock lock];
}
if(!shouldContinue || [self paused]) {
[accessLock unlock];
inPeek = NO;
return NO;
}
if([[previousNode buffer] isEmpty] && [previousNode endOfStream] == YES) {
[accessLock unlock];
inPeek = NO;
return NO;
}
BOOL ret = [[previousNode buffer] peekFormat:format channelConfig:config];
[accessLock unlock];
inPeek = NO;
return ret;
}
- (BOOL)peekTimestamp:(double *_Nonnull)timestamp timeRatio:(double *_Nonnull)timeRatio {
inPeek = YES;
if(!shouldContinue || [self paused]) {
inPeek = NO;
return NO;
}
[accessLock lock];
while(shouldContinue && ![self paused] &&
[[previousNode buffer] isEmpty] && [previousNode endOfStream] == NO) {
[accessLock unlock];
[writeSemaphore signal];
[[previousNode readSemaphore] timedWait:2000];
[accessLock lock];
}
if(!shouldContinue || [self paused]) {
[accessLock unlock];
inPeek = NO;
return NO;
}
if([[previousNode buffer] isEmpty] && [previousNode endOfStream] == YES) {
[accessLock unlock];
inPeek = NO;
return NO;
}
BOOL ret = [[previousNode buffer] peekTimestamp:timestamp timeRatio:timeRatio];
[accessLock unlock];
inPeek = NO;
return ret;
}
- (AudioChunk *)readChunk:(size_t)maxFrames {
inRead = YES;
if(!shouldContinue || [self paused]) {
inRead = NO;
return [[AudioChunk alloc] init];
}
[accessLock lock];
if([[previousNode buffer] isEmpty] && [previousNode endOfStream] == YES) {
endOfStream = YES;
while(shouldContinue && ![self paused] &&
[[previousNode buffer] isEmpty] && [previousNode endOfStream] == NO) {
[accessLock unlock];
[writeSemaphore signal];
[[previousNode readSemaphore] timedWait:2000];
[accessLock lock];
if([previousNode shouldReset] == YES) {
break;
}
}
if(!shouldContinue || [self paused]) {
[accessLock unlock];
inRead = NO;
return [[AudioChunk alloc] init];
}
if([[previousNode buffer] isEmpty] && [previousNode endOfStream] == YES) {
[accessLock unlock];
inRead = NO;
return [[AudioChunk alloc] init];
}
@ -128,7 +349,7 @@
shouldReset = YES;
[previousNode setShouldReset:NO];
[[previousNode semaphore] signal];
[[previousNode writeSemaphore] signal];
}
AudioChunk *ret;
@ -140,9 +361,203 @@
[accessLock unlock];
if([ret frameCount]) {
[[previousNode semaphore] signal];
[[previousNode writeSemaphore] signal];
}
#ifdef LOG_CHAINS
if(logFileIn) {
AudioChunk *chunkCopy = [ret copy];
size_t frameCount = [chunkCopy frameCount];
NSData *chunkData = [chunkCopy removeSamples:frameCount];
[logFileIn writeData:chunkData];
}
#endif
inRead = NO;
return ret;
}
- (AudioChunk *)readChunkAsFloat32:(size_t)maxFrames {
inRead = YES;
if(!shouldContinue || [self paused]) {
inRead = NO;
return [[AudioChunk alloc] init];
}
[accessLock lock];
while(shouldContinue && ![self paused] &&
[[previousNode buffer] isEmpty] && [previousNode endOfStream] == NO) {
[accessLock unlock];
[writeSemaphore signal];
[[previousNode readSemaphore] timedWait:2000];
[accessLock lock];
if([previousNode shouldReset] == YES) {
break;
}
}
if(!shouldContinue || [self paused]) {
[accessLock unlock];
inRead = NO;
return [[AudioChunk alloc] init];
}
if([[previousNode buffer] isEmpty] && [previousNode endOfStream] == YES) {
[accessLock unlock];
inRead = NO;
return [[AudioChunk alloc] init];
}
if([previousNode shouldReset] == YES) {
@autoreleasepool {
[buffer reset];
}
shouldReset = YES;
[previousNode setShouldReset:NO];
[[previousNode writeSemaphore] signal];
}
AudioChunk *ret;
@autoreleasepool {
ret = [[previousNode buffer] removeSamplesAsFloat32:maxFrames];
}
[accessLock unlock];
if([ret frameCount]) {
[[previousNode writeSemaphore] signal];
}
#ifdef LOG_CHAINS
if(logFileIn) {
AudioChunk *chunkCopy = [ret copy];
size_t frameCount = [chunkCopy frameCount];
NSData *chunkData = [chunkCopy removeSamples:frameCount];
[logFileIn writeData:chunkData];
}
#endif
inRead = NO;
return ret;
}
- (AudioChunk *)readAndMergeChunks:(size_t)maxFrames {
inMerge = YES;
if(!shouldContinue || [self paused]) {
inMerge = NO;
return [[AudioChunk alloc] init];
}
[accessLock lock];
if([[previousNode buffer] isEmpty] && [previousNode endOfStream] == YES) {
[accessLock unlock];
inMerge = NO;
return [[AudioChunk alloc] init];
}
AudioChunk *ret;
@autoreleasepool {
ret = [[previousNode buffer] removeAndMergeSamples:maxFrames callBlock:^BOOL{
if([previousNode shouldReset] == YES) {
@autoreleasepool {
[buffer reset];
}
shouldReset = YES;
[previousNode setShouldReset:NO];
}
[accessLock unlock];
[[previousNode writeSemaphore] signal];
[[previousNode readSemaphore] timedWait:2000];
[accessLock lock];
return !shouldContinue || [self paused] || ([[previousNode buffer] isEmpty] && [previousNode endOfStream] == YES);
}];
}
[accessLock unlock];
if([ret frameCount]) {
[[previousNode writeSemaphore] signal];
#ifdef LOG_CHAINS
if(logFileIn) {
AudioChunk *chunkCopy = [ret copy];
size_t frameCount = [chunkCopy frameCount];
NSData *chunkData = [chunkCopy removeSamples:frameCount];
[logFileIn writeData:chunkData];
}
#endif
}
inMerge = NO;
return ret;
}
- (AudioChunk *)readAndMergeChunksAsFloat32:(size_t)maxFrames {
inMerge = YES;
if(!shouldContinue || [self paused]) {
inMerge = NO;
return [[AudioChunk alloc] init];
}
[accessLock lock];
if([[previousNode buffer] isEmpty] && [previousNode endOfStream] == YES) {
[accessLock unlock];
inMerge = NO;
return [[AudioChunk alloc] init];
}
AudioChunk *ret;
@autoreleasepool {
ret = [[previousNode buffer] removeAndMergeSamplesAsFloat32:maxFrames callBlock:^BOOL{
if([previousNode shouldReset] == YES) {
@autoreleasepool {
[buffer reset];
}
shouldReset = YES;
[previousNode setShouldReset:NO];
}
[accessLock unlock];
[[previousNode writeSemaphore] signal];
[[previousNode readSemaphore] timedWait:2000];
[accessLock lock];
return !shouldContinue || [self paused] || ([[previousNode buffer] isEmpty] && [previousNode endOfStream] == YES);
}];
}
[accessLock unlock];
if([ret frameCount]) {
[[previousNode writeSemaphore] signal];
#ifdef LOG_CHAINS
if(logFileIn) {
AudioChunk *chunkCopy = [ret copy];
size_t frameCount = [chunkCopy frameCount];
NSData *chunkData = [chunkCopy removeSamples:frameCount];
[logFileIn writeData:chunkData];
}
#endif
}
inMerge = NO;
return ret;
}
@ -181,8 +596,31 @@
}
}
- (Semaphore *)semaphore {
return semaphore;
- (void)lockedResetBuffer {
@autoreleasepool {
[buffer reset];
}
}
- (void)unlockedResetBuffer {
@autoreleasepool {
[accessLock lock];
[buffer reset];
[accessLock unlock];
}
}
// Implementations should override
- (BOOL)paused {
return NO;
}
- (Semaphore *)writeSemaphore {
return writeSemaphore;
}
- (Semaphore *)readSemaphore {
return readSemaphore;
}
- (BOOL)endOfStream {
@ -205,4 +643,23 @@
return 0.0;
}
// Reset everything in the chain
- (void)resetBackwards {
[accessLock lock];
if(buffer) {
[self lockedResetBuffer];
[writeSemaphore signal];
[readSemaphore signal];
}
Node *node = previousNode;
while(node) {
[node unlockedResetBuffer];
[node setShouldReset:YES];
[[node writeSemaphore] signal];
[[node readSemaphore] signal];
node = [node previousNode];
}
[accessLock unlock];
}
@end

View file

@ -12,8 +12,8 @@
#import <AudioUnit/AudioUnit.h>
#import <CoreAudio/AudioHardware.h>
#import "Node.h"
#import "OutputCoreAudio.h"
#import <CogAudio/Node.h>
#import <CogAudio/OutputCoreAudio.h>
@interface OutputNode : Node {
AudioStreamBasicDescription format;
@ -28,38 +28,49 @@
BOOL intervalReported;
}
- (void)beginEqualizer:(AudioUnit)eq;
- (void)refreshEqualizer:(AudioUnit)eq;
- (void)endEqualizer:(AudioUnit)eq;
- (double)amountPlayed;
- (double)amountPlayedInterval;
- (void)incrementAmountPlayed:(double)seconds;
- (void)setAmountPlayed:(double)seconds;
- (void)resetAmountPlayed;
- (void)resetAmountPlayedInterval;
- (BOOL)selectNextBuffer;
- (void)endOfInputPlayed;
- (BOOL)endOfStream;
- (BOOL)chainQueueHasTracks;
- (double)secondsBuffered;
- (void)setup;
- (void)setupWithInterval:(BOOL)resumeInterval;
- (void)process;
- (void)close;
- (void)seek:(double)time;
- (void)fadeOut;
- (void)fadeOutBackground;
- (void)fadeIn;
- (AudioChunk *)readChunk:(size_t)amount;
- (void)setFormat:(AudioStreamBasicDescription *)f channelConfig:(uint32_t)channelConfig;
- (AudioStreamBasicDescription)format;
- (uint32_t)config;
- (AudioStreamBasicDescription)deviceFormat;
- (uint32_t)deviceChannelConfig;
- (double)volume;
- (void)setVolume:(double)v;
- (void)setShouldContinue:(BOOL)s;
- (void)setShouldPlayOutBuffer:(BOOL)s;
- (void)pause;
- (void)resume;
@ -69,4 +80,12 @@
- (void)restartPlaybackAtCurrentPosition;
- (double)latency;
- (double)getVisLatency;
- (double)getTotalLatency;
- (id)controller;
- (id)downmix;
@end

View file

@ -11,21 +11,70 @@
#import "BufferChain.h"
#import "OutputCoreAudio.h"
#import "DSPRubberbandNode.h"
#import "DSPFSurroundNode.h"
#import "DSPHRTFNode.h"
#import "DSPEqualizerNode.h"
#import "VisualizationNode.h"
#import "DSPDownmixNode.h"
#import "Logging.h"
@implementation OutputNode
@implementation OutputNode {
BOOL DSPsLaunched;
Node *previousInput;
DSPRubberbandNode *rubberbandNode;
DSPFSurroundNode *fsurroundNode;
DSPHRTFNode *hrtfNode;
DSPEqualizerNode *equalizerNode;
DSPDownmixNode *downmixNode;
VisualizationNode *visualizationNode;
}
- (void)setup {
amountPlayed = 0.0;
amountPlayedInterval = 0.0;
[self setupWithInterval:NO];
}
- (void)setupWithInterval:(BOOL)resumeInterval {
if(!resumeInterval) {
amountPlayed = 0.0;
amountPlayedInterval = 0.0;
intervalReported = NO;
}
paused = YES;
started = NO;
intervalReported = NO;
output = [[OutputCoreAudio alloc] initWithController:self];
[output setup];
if(!DSPsLaunched) {
rubberbandNode = [[DSPRubberbandNode alloc] initWithController:self previous:nil latency:0.1];
if(!rubberbandNode) return;
fsurroundNode = [[DSPFSurroundNode alloc] initWithController:self previous:rubberbandNode latency:0.03];
if(!fsurroundNode) return;
equalizerNode = [[DSPEqualizerNode alloc] initWithController:self previous:fsurroundNode latency:0.03];
if(!equalizerNode) return;
hrtfNode = [[DSPHRTFNode alloc] initWithController:self previous:equalizerNode latency:0.03];
if(!hrtfNode) return;
downmixNode = [[DSPDownmixNode alloc] initWithController:self previous:hrtfNode latency:0.03];
if(!downmixNode) return;
// Approximately double the chunk size for Vis at 44100Hz
visualizationNode = [[VisualizationNode alloc] initWithController:self previous:downmixNode latency:8192.0 / 44100.0];
if(!visualizationNode) return;
[self setPreviousNode:visualizationNode];
DSPsLaunched = YES;
[self launchDSPs];
previousInput = nil;
}
}
- (void)seek:(double)time {
@ -50,6 +99,19 @@
[output resume];
}
- (void)fadeOut {
[output fadeOut];
}
- (void)fadeOutBackground {
[output fadeOutBackground];
}
- (void)fadeIn {
[self reconnectInputAndReplumb];
[output fadeIn];
}
- (void)incrementAmountPlayed:(double)seconds {
amountPlayed += seconds;
amountPlayedInterval += seconds;
@ -59,6 +121,15 @@
}
}
- (void)setAmountPlayed:(double)seconds {
double delta = seconds - amountPlayed;
if(delta > 0.0 && delta < 5.0) {
[self incrementAmountPlayed:delta];
} else if(delta) {
amountPlayed = seconds;
}
}
- (void)resetAmountPlayed {
amountPlayed = 0;
}
@ -68,6 +139,14 @@
intervalReported = NO;
}
- (BOOL)selectNextBuffer {
BOOL ret = [controller selectNextBuffer];
if(!ret) {
[self reconnectInputAndReplumb];
}
return ret;
}
- (void)endOfInputPlayed {
if(!intervalReported) {
intervalReported = YES;
@ -85,17 +164,74 @@
return [buffer listDuration];
}
- (NSArray *)DSPs {
if(DSPsLaunched) {
return @[rubberbandNode, fsurroundNode, equalizerNode, hrtfNode, downmixNode, visualizationNode];
} else {
return @[];
}
}
- (BOOL)reconnectInput {
Node *finalNode = nil;
if(rubberbandNode) {
finalNode = [[controller bufferChain] finalNode];
[rubberbandNode setPreviousNode:finalNode];
}
return !!finalNode;
}
- (void)reconnectInputAndReplumb {
Node *finalNode = nil;
if(rubberbandNode) {
finalNode = [[controller bufferChain] finalNode];
[rubberbandNode setPreviousNode:finalNode];
}
NSArray *DSPs = [self DSPs];
for (Node *node in DSPs) {
[node setEndOfStream:NO];
[node setShouldContinue:YES];
}
}
- (void)launchDSPs {
NSArray *DSPs = [self DSPs];
for (Node *node in DSPs) {
[node launchThread];
}
}
- (AudioChunk *)readChunk:(size_t)amount {
@autoreleasepool {
[self setPreviousNode:[[controller bufferChain] finalNode]];
if([self reconnectInput]) {
AudioChunk *ret = [super readChunk:amount];
AudioChunk *ret = [super readChunk:amount];
if((!ret || ![ret frameCount]) && [previousNode endOfStream]) {
endOfStream = YES;
}
/* if (n == 0) {
DLog(@"Output Buffer dry!");
}
*/
return ret;
return ret;
} else {
return [[AudioChunk alloc] init];
}
}
}
- (BOOL)peekFormat:(nonnull AudioStreamBasicDescription *)format channelConfig:(nonnull uint32_t *)config {
@autoreleasepool {
if([self reconnectInput]) {
BOOL ret = [super peekFormat:format channelConfig:config];
if(!ret && [previousNode endOfStream]) {
endOfStream = YES;
}
return ret;
} else {
return NO;
}
}
}
@ -115,31 +251,59 @@
return config;
}
- (AudioStreamBasicDescription)deviceFormat {
return [output deviceFormat];
}
- (uint32_t)deviceChannelConfig {
return [output deviceChannelConfig];
}
- (void)setFormat:(AudioStreamBasicDescription *)f channelConfig:(uint32_t)channelConfig {
if(!shouldContinue) return;
format = *f;
config = channelConfig;
// Calculate a ratio and add to double(seconds) instead, as format may change
// double oldSampleRatio = sampleRatio;
BufferChain *bufferChain = [controller bufferChain];
AudioPlayer *audioPlayer = controller;
BufferChain *bufferChain = [audioPlayer bufferChain];
if(bufferChain) {
ConverterNode *converter = [bufferChain converter];
AudioStreamBasicDescription outputFormat;
uint32_t outputChannelConfig;
BOOL formatChanged = NO;
if(converter) {
// This clears the resampler buffer, but not the input buffer
// We also have to jump the play position ahead accounting for
// the data we are flushing
amountPlayed += [[converter buffer] listDuration];
AudioStreamBasicDescription inf = [bufferChain inputFormat];
uint32_t config = [bufferChain inputConfig];
format.mChannelsPerFrame = inf.mChannelsPerFrame;
format.mBytesPerFrame = ((inf.mBitsPerChannel + 7) / 8) * format.mChannelsPerFrame;
format.mBytesPerPacket = format.mBytesPerFrame * format.mFramesPerPacket;
channelConfig = config;
[converter setOutputFormat:format
outputConfig:channelConfig];
[converter inputFormatDidChange:[bufferChain inputFormat] inputConfig:[bufferChain inputConfig]];
AudioStreamBasicDescription converterFormat = [converter nodeFormat];
if(memcmp(&converterFormat, &format, sizeof(converterFormat)) != 0) {
formatChanged = YES;
}
}
if(downmixNode && output && !formatChanged) {
outputFormat = [output deviceFormat];
outputChannelConfig = [output deviceChannelConfig];
AudioStreamBasicDescription currentOutputFormat = [downmixNode nodeFormat];
uint32_t currentOutputChannelConfig = [downmixNode nodeChannelConfig];
if(memcmp(&currentOutputFormat, &outputFormat, sizeof(currentOutputFormat)) != 0 ||
currentOutputChannelConfig != outputChannelConfig) {
formatChanged = YES;
}
}
if(formatChanged) {
InputNode *inputNode = [bufferChain inputNode];
if(converter) {
[converter setOutputFormat:format];
}
if(downmixNode && output) {
[downmixNode setOutputFormat:[output deviceFormat] withChannelConfig:[output deviceChannelConfig]];
}
if(inputNode) {
AudioStreamBasicDescription inputFormat = [inputNode nodeFormat];
if(converter) {
[converter inputFormatDidChange:inputFormat inputConfig:[inputNode nodeChannelConfig]];
}
[inputNode seek:(long)(amountPlayed * inputFormat.mSampleRate)];
}
}
}
}
@ -147,6 +311,24 @@
- (void)close {
[output stop];
output = nil;
if(DSPsLaunched) {
NSArray *DSPs = [self DSPs];
for(Node *node in DSPs) {
[node setShouldContinue:NO];
}
previousNode = nil;
visualizationNode = nil;
downmixNode = nil;
hrtfNode = nil;
fsurroundNode = nil;
rubberbandNode = nil;
previousInput = nil;
DSPsLaunched = NO;
}
}
- (double)volume {
return [output volume];
}
- (void)setVolume:(double)v {
@ -156,26 +338,22 @@
- (void)setShouldContinue:(BOOL)s {
[super setShouldContinue:s];
NSArray *DSPs = [self DSPs];
for(Node *node in DSPs) {
[node setShouldContinue:s];
}
// if (s == NO)
// [output stop];
}
- (void)setShouldPlayOutBuffer:(BOOL)s {
[output setShouldPlayOutBuffer:s];
}
- (BOOL)isPaused {
return paused;
}
- (void)beginEqualizer:(AudioUnit)eq {
[controller beginEqualizer:eq];
}
- (void)refreshEqualizer:(AudioUnit)eq {
[controller refreshEqualizer:eq];
}
- (void)endEqualizer:(AudioUnit)eq {
[controller endEqualizer:eq];
}
- (void)sustainHDCD {
[output sustainHDCD];
}
@ -184,4 +362,29 @@
[controller restartPlaybackAtCurrentPosition];
}
- (double)latency {
double latency = 0.0;
NSArray *DSPs = [self DSPs];
for(Node *node in DSPs) {
latency += [node secondsBuffered];
}
return [output latency] + latency;
}
- (double)getVisLatency {
return [output latency] + [visualizationNode secondsBuffered];
}
- (double)getTotalLatency {
return [[controller bufferChain] secondsBuffered] + [self latency];
}
- (id)controller {
return controller;
}
- (id)downmix {
return downmixNode;
}
@end

View file

@ -0,0 +1,35 @@
//
// VisualizationNode.h
// CogAudio
//
// Created by Christopher Snowhill on 2/12/25.
//
#ifndef VisualizationNode_h
#define VisualizationNode_h
#import <CogAudio/Node.h>
@interface VisualizationNode : Node {
}
- (id _Nullable)initWithController:(id _Nonnull)c previous:(id _Nullable)p latency:(double)latency;
- (void)threadEntry:(id _Nullable)arg;
- (BOOL)setup;
- (void)cleanUp;
- (BOOL)paused;
- (void)resetBuffer;
- (void)setShouldContinue:(BOOL)s;
- (void)process;
- (double)secondsBuffered;
@end
#endif /* VisualizationNode_h */

View file

@ -0,0 +1,273 @@
//
// VisualizationNode.m
// CogAudio Framework
//
// Created by Christopher Snowhill on 2/12/25.
//
#import <Foundation/Foundation.h>
#import <AudioToolbox/AudioToolbox.h>
#import <Accelerate/Accelerate.h>
#import "Downmix.h"
#import <CogAudio/VisualizationController.h>
#import "BufferChain.h"
#import "Logging.h"
#import "rsstate.h"
#import "VisualizationNode.h"
@implementation VisualizationNode {
void *rs;
double lastVisRate;
BOOL processEntered;
BOOL stopping;
BOOL paused;
BOOL threadTerminated;
AudioStreamBasicDescription inputFormat;
AudioStreamBasicDescription visFormat; // Mono format for vis
uint32_t inputChannelConfig;
uint32_t visChannelConfig;
size_t resamplerRemain;
DownmixProcessor *downmixer;
VisualizationController *visController;
float visAudio[512];
float resamplerInput[8192];
float visTemp[8192];
}
- (id _Nullable)initWithController:(id _Nonnull)c previous:(id _Nullable)p latency:(double)latency {
self = [super init];
if(self) {
buffer = [[ChunkList alloc] initWithMaximumDuration:latency];
writeSemaphore = [[Semaphore alloc] init];
readSemaphore = [[Semaphore alloc] init];
accessLock = [[NSLock alloc] init];
initialBufferFilled = NO;
controller = c;
endOfStream = NO;
shouldContinue = YES;
nodeChannelConfig = 0;
nodeLossless = NO;
durationPrebuffer = latency * 0.25;
visController = [VisualizationController sharedController];
inWrite = NO;
inPeek = NO;
inRead = NO;
inMerge = NO;
[self setPreviousNode:p];
}
return self;
}
- (void)dealloc {
DLog(@"Visualization node dealloc");
[self setShouldContinue:NO];
[self cleanUp];
[super cleanUp];
}
// Visualization thread should be fairly high priority, too
- (void)threadEntry:(id _Nullable)arg {
@autoreleasepool {
NSThread *currentThread = [NSThread currentThread];
[currentThread setThreadPriority:0.75];
[currentThread setQualityOfService:NSQualityOfServiceUserInitiated];
threadTerminated = NO;
[self process];
threadTerminated = YES;
}
}
- (void)resetBuffer {
paused = YES;
while(processEntered) {
usleep(500);
}
[buffer reset];
[self fullShutdown];
paused = NO;
}
- (double)secondsBuffered {
return [buffer listDuration];
}
- (void)setShouldContinue:(BOOL)s {
BOOL currentShouldContinue = shouldContinue;
shouldContinue = s;
if(!currentShouldContinue && s && threadTerminated) {
[self launchThread];
}
}
- (BOOL)setup {
if(fabs(inputFormat.mSampleRate - 44100.0) > 1e-6) {
rs = rsstate_new(1, inputFormat.mSampleRate, 44100.0);
if(!rs) {
return NO;
}
resamplerRemain = 0;
}
visFormat = inputFormat;
visFormat.mChannelsPerFrame = 1;
visFormat.mBytesPerFrame = sizeof(float);
visFormat.mBytesPerPacket = visFormat.mBytesPerFrame * visFormat.mFramesPerPacket;
visChannelConfig = AudioChannelFrontCenter;
downmixer = [[DownmixProcessor alloc] initWithInputFormat:inputFormat inputConfig:inputChannelConfig andOutputFormat:visFormat outputConfig:visChannelConfig];
if(!downmixer) {
return NO;
}
return YES;
}
- (void)cleanUp {
stopping = YES;
while(processEntered) {
usleep(500);
}
[self fullShutdown];
}
- (void)fullShutdown {
if(rs) {
rsstate_delete(rs);
rs = NULL;
}
downmixer = nil;
}
- (BOOL)paused {
return paused;
}
- (void)process {
while([self shouldContinue] == YES) {
if(paused || endOfStream) {
usleep(500);
continue;
}
@autoreleasepool {
AudioChunk *chunk = nil;
chunk = [self readAndMergeChunksAsFloat32:512];
if(!chunk || ![chunk frameCount]) {
if([previousNode endOfStream] == YES) {
usleep(500);
endOfStream = YES;
continue;
}
} else {
[self processVis:[chunk copy]];
[self writeChunk:chunk];
chunk = nil;
}
}
}
endOfStream = YES;
}
- (void)postVisPCM:(const float *)visTemp amount:(size_t)samples {
[visController postVisPCM:visTemp amount:(int)samples];
}
- (void)processVis:(AudioChunk *)chunk {
processEntered = YES;
if(paused) {
processEntered = NO;
return;
}
AudioStreamBasicDescription format = [chunk format];
uint32_t channelConfig = [chunk channelConfig];
[visController postSampleRate:44100.0];
if(!rs || !downmixer ||
memcmp(&format, &inputFormat, sizeof(format)) != 0 ||
channelConfig != inputChannelConfig) {
if(rs) {
while(!stopping) {
int samplesFlushed;
samplesFlushed = (int)rsstate_flush(rs, &visTemp[0], 8192);
if(samplesFlushed > 1) {
[self postVisPCM:visTemp amount:samplesFlushed];
} else {
break;
}
}
}
[self fullShutdown];
inputFormat = format;
inputChannelConfig = channelConfig;
if(![self setup]) {
processEntered = NO;
return;
}
}
size_t frameCount = [chunk frameCount];
NSData *sampleData = [chunk removeSamples:frameCount];
[downmixer process:[sampleData bytes] frameCount:frameCount output:&visAudio[0]];
if(rs) {
int samplesProcessed;
size_t totalDone = 0;
size_t inDone = 0;
size_t visFrameCount = frameCount;
do {
if(stopping) {
break;
}
int visTodo = (int)MIN(visFrameCount, resamplerRemain + visFrameCount - 8192);
if(visTodo) {
cblas_scopy(visTodo, &visAudio[0], 1, &resamplerInput[resamplerRemain], 1);
}
visTodo += resamplerRemain;
resamplerRemain = 0;
samplesProcessed = (int)rsstate_resample(rs, &resamplerInput[0], visTodo, &inDone, &visTemp[0], 8192);
resamplerRemain = (int)(visTodo - inDone);
if(resamplerRemain && inDone) {
memmove(&resamplerInput[0], &resamplerInput[inDone], resamplerRemain * sizeof(float));
}
if(samplesProcessed) {
[self postVisPCM:&visTemp[0] amount:samplesProcessed];
}
totalDone += inDone;
visFrameCount -= inDone;
} while(samplesProcessed && visFrameCount);
} else {
[self postVisPCM:&visAudio[0] amount:frameCount];
}
processEntered = NO;
}
@end

View file

@ -0,0 +1 @@
#import "ThirdParty/deadbeef/fft.h"

View file

@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 46;
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
@ -25,11 +25,9 @@
17D21CA80B8BE4BA00D1EBDE /* Node.m in Sources */ = {isa = PBXBuildFile; fileRef = 17D21C7D0B8BE4BA00D1EBDE /* Node.m */; };
17D21CA90B8BE4BA00D1EBDE /* OutputNode.h in Headers */ = {isa = PBXBuildFile; fileRef = 17D21C7E0B8BE4BA00D1EBDE /* OutputNode.h */; settings = {ATTRIBUTES = (Public, ); }; };
17D21CAA0B8BE4BA00D1EBDE /* OutputNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 17D21C7F0B8BE4BA00D1EBDE /* OutputNode.m */; };
17D21CC50B8BE4BA00D1EBDE /* OutputCoreAudio.h in Headers */ = {isa = PBXBuildFile; fileRef = 17D21C9C0B8BE4BA00D1EBDE /* OutputCoreAudio.h */; settings = {ATTRIBUTES = (Public, ); }; };
17D21CC60B8BE4BA00D1EBDE /* OutputCoreAudio.m in Sources */ = {isa = PBXBuildFile; fileRef = 17D21C9D0B8BE4BA00D1EBDE /* OutputCoreAudio.m */; };
17D21CC70B8BE4BA00D1EBDE /* Status.h in Headers */ = {isa = PBXBuildFile; fileRef = 17D21C9E0B8BE4BA00D1EBDE /* Status.h */; settings = {ATTRIBUTES = (Public, ); }; };
17D21CF30B8BE5EF00D1EBDE /* Semaphore.h in Headers */ = {isa = PBXBuildFile; fileRef = 17D21CF10B8BE5EF00D1EBDE /* Semaphore.h */; settings = {ATTRIBUTES = (Public, ); }; };
17D21CF40B8BE5EF00D1EBDE /* Semaphore.m in Sources */ = {isa = PBXBuildFile; fileRef = 17D21CF20B8BE5EF00D1EBDE /* Semaphore.m */; };
17D21CF30B8BE5EF00D1EBDE /* CogSemaphore.h in Headers */ = {isa = PBXBuildFile; fileRef = 17D21CF10B8BE5EF00D1EBDE /* CogSemaphore.h */; settings = {ATTRIBUTES = (Public, ); }; };
17D21CF40B8BE5EF00D1EBDE /* CogSemaphore.m in Sources */ = {isa = PBXBuildFile; fileRef = 17D21CF20B8BE5EF00D1EBDE /* CogSemaphore.m */; };
17D21DAD0B8BE76800D1EBDE /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 17D21DA90B8BE76800D1EBDE /* AudioToolbox.framework */; };
17D21DAE0B8BE76800D1EBDE /* AudioUnit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 17D21DAA0B8BE76800D1EBDE /* AudioUnit.framework */; };
17D21DAF0B8BE76800D1EBDE /* CoreAudio.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 17D21DAB0B8BE76800D1EBDE /* CoreAudio.framework */; };
@ -41,89 +39,86 @@
17F94DD50B8D0F7000A34E87 /* PluginController.h in Headers */ = {isa = PBXBuildFile; fileRef = 17F94DD30B8D0F7000A34E87 /* PluginController.h */; settings = {ATTRIBUTES = (Public, ); }; };
17F94DD60B8D0F7000A34E87 /* PluginController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 17F94DD40B8D0F7000A34E87 /* PluginController.mm */; };
17F94DDD0B8D101100A34E87 /* Plugin.h in Headers */ = {isa = PBXBuildFile; fileRef = 17F94DDC0B8D101100A34E87 /* Plugin.h */; settings = {ATTRIBUTES = (Public, ); }; };
831A50142865A7FD0049CFE4 /* rsstate.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 831A50132865A7FD0049CFE4 /* rsstate.hpp */; };
831A50162865A8800049CFE4 /* rsstate.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 831A50152865A8800049CFE4 /* rsstate.cpp */; };
831A50182865A8B30049CFE4 /* rsstate.h in Headers */ = {isa = PBXBuildFile; fileRef = 831A50172865A8B30049CFE4 /* rsstate.h */; };
8328995327CB511000D7F028 /* RedundantPlaylistDataStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 8328995127CB510F00D7F028 /* RedundantPlaylistDataStore.m */; };
8328995427CB511000D7F028 /* RedundantPlaylistDataStore.h in Headers */ = {isa = PBXBuildFile; fileRef = 8328995227CB511000D7F028 /* RedundantPlaylistDataStore.h */; };
8328995727CB51B700D7F028 /* SHA256Digest.h in Headers */ = {isa = PBXBuildFile; fileRef = 8328995527CB51B700D7F028 /* SHA256Digest.h */; };
8328995827CB51B700D7F028 /* SHA256Digest.m in Sources */ = {isa = PBXBuildFile; fileRef = 8328995627CB51B700D7F028 /* SHA256Digest.m */; };
8328995A27CB51C900D7F028 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8328995927CB51C900D7F028 /* Security.framework */; };
833442422D6EFA6700C51D38 /* VisualizationController.h in Headers */ = {isa = PBXBuildFile; fileRef = 833442402D6EFA6700C51D38 /* VisualizationController.h */; settings = {ATTRIBUTES = (Public, ); }; };
833442432D6EFA6700C51D38 /* VisualizationController.m in Sources */ = {isa = PBXBuildFile; fileRef = 833442412D6EFA6700C51D38 /* VisualizationController.m */; };
833738EA2D5EA52500278628 /* DSPDownmixNode.h in Headers */ = {isa = PBXBuildFile; fileRef = 833738E92D5EA52500278628 /* DSPDownmixNode.h */; settings = {ATTRIBUTES = (Public, ); }; };
833738EC2D5EA53500278628 /* DSPDownmixNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 833738EB2D5EA53500278628 /* DSPDownmixNode.m */; };
833738EF2D5EA5B700278628 /* Downmix.m in Sources */ = {isa = PBXBuildFile; fileRef = 833738EE2D5EA5B700278628 /* Downmix.m */; };
833738F02D5EA5B700278628 /* Downmix.h in Headers */ = {isa = PBXBuildFile; fileRef = 833738ED2D5EA5B700278628 /* Downmix.h */; settings = {ATTRIBUTES = (Public, ); }; };
8347C7412796C58800FA8A7D /* NSFileHandle+CreateFile.h in Headers */ = {isa = PBXBuildFile; fileRef = 8347C73F2796C58800FA8A7D /* NSFileHandle+CreateFile.h */; };
8347C7422796C58800FA8A7D /* NSFileHandle+CreateFile.m in Sources */ = {isa = PBXBuildFile; fileRef = 8347C7402796C58800FA8A7D /* NSFileHandle+CreateFile.m */; };
834FD4EB27AF8F380063BC83 /* AudioChunk.h in Headers */ = {isa = PBXBuildFile; fileRef = 834FD4EA27AF8F380063BC83 /* AudioChunk.h */; };
834A41A9287A90AB00EB9D9B /* freesurround_decoder.h in Headers */ = {isa = PBXBuildFile; fileRef = 834A41A5287A90AB00EB9D9B /* freesurround_decoder.h */; };
834A41AA287A90AB00EB9D9B /* freesurround_decoder.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 834A41A6287A90AB00EB9D9B /* freesurround_decoder.cpp */; };
834A41AB287A90AB00EB9D9B /* channelmaps.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 834A41A7287A90AB00EB9D9B /* channelmaps.cpp */; };
834A41AC287A90AB00EB9D9B /* channelmaps.h in Headers */ = {isa = PBXBuildFile; fileRef = 834A41A8287A90AB00EB9D9B /* channelmaps.h */; };
834FD4EB27AF8F380063BC83 /* AudioChunk.h in Headers */ = {isa = PBXBuildFile; fileRef = 834FD4EA27AF8F380063BC83 /* AudioChunk.h */; settings = {ATTRIBUTES = (Public, ); }; };
834FD4ED27AF91220063BC83 /* AudioChunk.m in Sources */ = {isa = PBXBuildFile; fileRef = 834FD4EC27AF91220063BC83 /* AudioChunk.m */; };
834FD4F027AF93680063BC83 /* ChunkList.h in Headers */ = {isa = PBXBuildFile; fileRef = 834FD4EE27AF93680063BC83 /* ChunkList.h */; };
834FD4F027AF93680063BC83 /* ChunkList.h in Headers */ = {isa = PBXBuildFile; fileRef = 834FD4EE27AF93680063BC83 /* ChunkList.h */; settings = {ATTRIBUTES = (Public, ); }; };
834FD4F127AF93680063BC83 /* ChunkList.m in Sources */ = {isa = PBXBuildFile; fileRef = 834FD4EF27AF93680063BC83 /* ChunkList.m */; };
834FD4F427AFA2150063BC83 /* Downmix.h in Headers */ = {isa = PBXBuildFile; fileRef = 834FD4F227AFA2150063BC83 /* Downmix.h */; };
834FD4F527AFA2150063BC83 /* Downmix.m in Sources */ = {isa = PBXBuildFile; fileRef = 834FD4F327AFA2150063BC83 /* Downmix.m */; };
835C88A82797D4D400E28EAE /* LICENSE.LGPL in Resources */ = {isa = PBXBuildFile; fileRef = 835C88A42797D4D400E28EAE /* LICENSE.LGPL */; };
835C88A92797D4D400E28EAE /* License.txt in Resources */ = {isa = PBXBuildFile; fileRef = 835C88A52797D4D400E28EAE /* License.txt */; };
835C88AA2797D4D400E28EAE /* lpc.c in Sources */ = {isa = PBXBuildFile; fileRef = 835C88A62797D4D400E28EAE /* lpc.c */; };
835C88AB2797D4D400E28EAE /* lpc.h in Headers */ = {isa = PBXBuildFile; fileRef = 835C88A72797D4D400E28EAE /* lpc.h */; };
835C88AD2797DA5800E28EAE /* util.h in Headers */ = {isa = PBXBuildFile; fileRef = 835C88AC2797DA5800E28EAE /* util.h */; };
8350416D28646149006B32CC /* CoreMedia.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8350416C28646149006B32CC /* CoreMedia.framework */; };
835C88B1279811A500E28EAE /* hdcd_decode2.h in Headers */ = {isa = PBXBuildFile; fileRef = 835C88AF279811A500E28EAE /* hdcd_decode2.h */; };
835C88B2279811A500E28EAE /* hdcd_decode2.c in Sources */ = {isa = PBXBuildFile; fileRef = 835C88B0279811A500E28EAE /* hdcd_decode2.c */; };
835EDD7B279FE23A001EDCCE /* HeadphoneFilter.mm in Sources */ = {isa = PBXBuildFile; fileRef = 835EDD7A279FE23A001EDCCE /* HeadphoneFilter.mm */; };
835EDD7D279FE307001EDCCE /* HeadphoneFilter.h in Headers */ = {isa = PBXBuildFile; fileRef = 835EDD7C279FE307001EDCCE /* HeadphoneFilter.h */; };
835DD2672ACAF1D90057E319 /* OutputCoreAudio.m in Sources */ = {isa = PBXBuildFile; fileRef = 835DD2652ACAF1D90057E319 /* OutputCoreAudio.m */; };
835DD2682ACAF1D90057E319 /* OutputCoreAudio.h in Headers */ = {isa = PBXBuildFile; fileRef = 835DD2662ACAF1D90057E319 /* OutputCoreAudio.h */; settings = {ATTRIBUTES = (Public, ); }; };
835DD2722ACAF5AD0057E319 /* lpc.h in Headers */ = {isa = PBXBuildFile; fileRef = 835DD26D2ACAF5AD0057E319 /* lpc.h */; };
835DD2732ACAF5AD0057E319 /* util.h in Headers */ = {isa = PBXBuildFile; fileRef = 835DD26E2ACAF5AD0057E319 /* util.h */; };
835DD2742ACAF5AD0057E319 /* lpc.c in Sources */ = {isa = PBXBuildFile; fileRef = 835DD26F2ACAF5AD0057E319 /* lpc.c */; };
835FAC5E27BCA14D00BA8562 /* BadSampleCleaner.h in Headers */ = {isa = PBXBuildFile; fileRef = 835FAC5C27BCA14D00BA8562 /* BadSampleCleaner.h */; };
835FAC5F27BCA14D00BA8562 /* BadSampleCleaner.m in Sources */ = {isa = PBXBuildFile; fileRef = 835FAC5D27BCA14D00BA8562 /* BadSampleCleaner.m */; };
8363BABE284E428F00E5C9DD /* pffft.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 8363BABD284E428F00E5C9DD /* pffft.cpp */; };
83725A9027AA16C90003F694 /* Accelerate.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 83725A7B27AA0D8A0003F694 /* Accelerate.framework */; };
83725A9127AA16D50003F694 /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 83725A7C27AA0D8E0003F694 /* AVFoundation.framework */; };
8377C64C27B8C51500E8BC0F /* fft_accelerate.c in Sources */ = {isa = PBXBuildFile; fileRef = 8377C64B27B8C51500E8BC0F /* fft_accelerate.c */; };
8377C64E27B8C54400E8BC0F /* fft.h in Headers */ = {isa = PBXBuildFile; fileRef = 8377C64D27B8C54400E8BC0F /* fft.h */; };
8377C65227B8CAD100E8BC0F /* VisualizationController.h in Headers */ = {isa = PBXBuildFile; fileRef = 8377C65027B8CAD100E8BC0F /* VisualizationController.h */; };
8377C65327B8CAD100E8BC0F /* VisualizationController.m in Sources */ = {isa = PBXBuildFile; fileRef = 8377C65127B8CAD100E8BC0F /* VisualizationController.m */; };
8384912718080FF100E7332D /* Logging.h in Headers */ = {isa = PBXBuildFile; fileRef = 8384912618080FF100E7332D /* Logging.h */; };
838A33722D06A97D00D0D770 /* librubberband.3.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 838A33712D06A97D00D0D770 /* librubberband.3.dylib */; };
839065F32853338700636FBB /* dsd2float.h in Headers */ = {isa = PBXBuildFile; fileRef = 839065F22853338700636FBB /* dsd2float.h */; };
839366671815923C006DD712 /* CogPluginMulti.h in Headers */ = {isa = PBXBuildFile; fileRef = 839366651815923C006DD712 /* CogPluginMulti.h */; };
839366681815923C006DD712 /* CogPluginMulti.m in Sources */ = {isa = PBXBuildFile; fileRef = 839366661815923C006DD712 /* CogPluginMulti.m */; };
8399CF2C27B5D1D5008751F1 /* NSDictionary+Merge.h in Headers */ = {isa = PBXBuildFile; fileRef = 8399CF2A27B5D1D4008751F1 /* NSDictionary+Merge.h */; };
8399CF2D27B5D1D5008751F1 /* NSDictionary+Merge.m in Sources */ = {isa = PBXBuildFile; fileRef = 8399CF2B27B5D1D4008751F1 /* NSDictionary+Merge.m */; };
83B69B752845DF6500D2435A /* pffft_double.h in Headers */ = {isa = PBXBuildFile; fileRef = 83B69B6B2845DF6500D2435A /* pffft_double.h */; };
83B69B762845DF6500D2435A /* pf_neon_double_from_avx.h in Headers */ = {isa = PBXBuildFile; fileRef = 83B69B6D2845DF6500D2435A /* pf_neon_double_from_avx.h */; };
83B69B772845DF6500D2435A /* pf_double.h in Headers */ = {isa = PBXBuildFile; fileRef = 83B69B6E2845DF6500D2435A /* pf_double.h */; };
83B69B782845DF6500D2435A /* pf_neon_double.h in Headers */ = {isa = PBXBuildFile; fileRef = 83B69B6F2845DF6500D2435A /* pf_neon_double.h */; };
83B69B792845DF6500D2435A /* pf_sse2_double.h in Headers */ = {isa = PBXBuildFile; fileRef = 83B69B702845DF6500D2435A /* pf_sse2_double.h */; };
83B69B7A2845DF6500D2435A /* pf_avx_double.h in Headers */ = {isa = PBXBuildFile; fileRef = 83B69B712845DF6500D2435A /* pf_avx_double.h */; };
83B69B7B2845DF6500D2435A /* pf_scalar_double.h in Headers */ = {isa = PBXBuildFile; fileRef = 83B69B722845DF6500D2435A /* pf_scalar_double.h */; };
83B69B7C2845DF6500D2435A /* pffft_priv_impl.h in Headers */ = {isa = PBXBuildFile; fileRef = 83B69B732845DF6500D2435A /* pffft_priv_impl.h */; };
83D40B572852CB3B003BB85C /* pffft_double.c in Sources */ = {isa = PBXBuildFile; fileRef = 83B69B742845DF6500D2435A /* pffft_double.c */; };
83F18B1E27D1E8EF00385946 /* CDSPHBDownsampler.h in Headers */ = {isa = PBXBuildFile; fileRef = 83F18ADF27D1E8EF00385946 /* CDSPHBDownsampler.h */; };
83F18B3327D1E8EF00385946 /* CDSPSincFilterGen.h in Headers */ = {isa = PBXBuildFile; fileRef = 83F18AF827D1E8EF00385946 /* CDSPSincFilterGen.h */; };
83F18B3427D1E8EF00385946 /* r8butil.h in Headers */ = {isa = PBXBuildFile; fileRef = 83F18AF927D1E8EF00385946 /* r8butil.h */; };
83F18B3627D1E8EF00385946 /* LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = 83F18AFB27D1E8EF00385946 /* LICENSE */; };
83F18B3727D1E8EF00385946 /* r8bbase.h in Headers */ = {isa = PBXBuildFile; fileRef = 83F18AFC27D1E8EF00385946 /* r8bbase.h */; };
83F18B3827D1E8EF00385946 /* CDSPFIRFilter.h in Headers */ = {isa = PBXBuildFile; fileRef = 83F18AFD27D1E8EF00385946 /* CDSPFIRFilter.h */; };
83F18B4227D1E8EF00385946 /* CDSPProcessor.h in Headers */ = {isa = PBXBuildFile; fileRef = 83F18B0827D1E8EF00385946 /* CDSPProcessor.h */; };
83F18B4327D1E8EF00385946 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 83F18B0927D1E8EF00385946 /* README.md */; };
83F18B4427D1E8EF00385946 /* fft4g.h in Headers */ = {isa = PBXBuildFile; fileRef = 83F18B0A27D1E8EF00385946 /* fft4g.h */; };
83F18B4527D1E8EF00385946 /* CDSPRealFFT.h in Headers */ = {isa = PBXBuildFile; fileRef = 83F18B0B27D1E8EF00385946 /* CDSPRealFFT.h */; };
83F18B4627D1E8EF00385946 /* CDSPFracInterpolator.h in Headers */ = {isa = PBXBuildFile; fileRef = 83F18B0C27D1E8EF00385946 /* CDSPFracInterpolator.h */; };
83F18B4E27D1E8F000385946 /* CDSPBlockConvolver.h in Headers */ = {isa = PBXBuildFile; fileRef = 83F18B1727D1E8EF00385946 /* CDSPBlockConvolver.h */; };
83F18B5027D1E8F000385946 /* r8bconf.h in Headers */ = {isa = PBXBuildFile; fileRef = 83F18B1927D1E8EF00385946 /* r8bconf.h */; };
83F18B5127D1E8F000385946 /* r8bbase.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 83F18B1A27D1E8EF00385946 /* r8bbase.cpp */; };
83F18B5227D1E8F000385946 /* pffft.h in Headers */ = {isa = PBXBuildFile; fileRef = 83F18B1B27D1E8EF00385946 /* pffft.h */; };
83F18B5327D1E8F000385946 /* CDSPResampler.h in Headers */ = {isa = PBXBuildFile; fileRef = 83F18B1C27D1E8EF00385946 /* CDSPResampler.h */; };
83F18B5427D1E8F000385946 /* CDSPHBUpsampler.h in Headers */ = {isa = PBXBuildFile; fileRef = 83F18B1D27D1E8EF00385946 /* CDSPHBUpsampler.h */; };
83F18B5627D1F5E900385946 /* r8bstate.h in Headers */ = {isa = PBXBuildFile; fileRef = 83F18B5527D1F5E900385946 /* r8bstate.h */; };
839E56E52879450300DFB5F4 /* HrtfData.h in Headers */ = {isa = PBXBuildFile; fileRef = 839E56E12879450300DFB5F4 /* HrtfData.h */; };
839E56E62879450300DFB5F4 /* Endianness.h in Headers */ = {isa = PBXBuildFile; fileRef = 839E56E22879450300DFB5F4 /* Endianness.h */; };
839E56E72879450300DFB5F4 /* HrtfData.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 839E56E32879450300DFB5F4 /* HrtfData.cpp */; };
839E56E82879450300DFB5F4 /* IHrtfData.h in Headers */ = {isa = PBXBuildFile; fileRef = 839E56E42879450300DFB5F4 /* IHrtfData.h */; };
839E56EA28794F6300DFB5F4 /* HrtfTypes.h in Headers */ = {isa = PBXBuildFile; fileRef = 839E56E928794F6300DFB5F4 /* HrtfTypes.h */; };
839E56F7287974A100DFB5F4 /* SandboxBroker.h in Headers */ = {isa = PBXBuildFile; fileRef = 839E56F6287974A100DFB5F4 /* SandboxBroker.h */; };
839E899E2D5DB9D500A13526 /* VisualizationNode.h in Headers */ = {isa = PBXBuildFile; fileRef = 839E899D2D5DB9D500A13526 /* VisualizationNode.h */; settings = {ATTRIBUTES = (Public, ); }; };
839E89A02D5DBA1700A13526 /* VisualizationNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 839E899F2D5DBA1700A13526 /* VisualizationNode.m */; };
83A3496A2D5C3F430096D530 /* DSPRubberbandNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 83A349682D5C3F430096D530 /* DSPRubberbandNode.m */; };
83A3496B2D5C3F430096D530 /* DSPRubberbandNode.h in Headers */ = {isa = PBXBuildFile; fileRef = 83A349672D5C3F430096D530 /* DSPRubberbandNode.h */; settings = {ATTRIBUTES = (Public, ); }; };
83A3496D2D5C40490096D530 /* DSPFSurroundNode.h in Headers */ = {isa = PBXBuildFile; fileRef = 83A3496C2D5C40490096D530 /* DSPFSurroundNode.h */; settings = {ATTRIBUTES = (Public, ); }; };
83A3496F2D5C405E0096D530 /* DSPFSurroundNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 83A3496E2D5C405E0096D530 /* DSPFSurroundNode.m */; };
83A349722D5C41810096D530 /* FSurroundFilter.mm in Sources */ = {isa = PBXBuildFile; fileRef = 83A349712D5C41810096D530 /* FSurroundFilter.mm */; };
83A349732D5C41810096D530 /* FSurroundFilter.h in Headers */ = {isa = PBXBuildFile; fileRef = 83A349702D5C41810096D530 /* FSurroundFilter.h */; settings = {ATTRIBUTES = (Public, ); }; };
83A349752D5C50A10096D530 /* DSPHRTFNode.h in Headers */ = {isa = PBXBuildFile; fileRef = 83A349742D5C50A10096D530 /* DSPHRTFNode.h */; settings = {ATTRIBUTES = (Public, ); }; };
83A349772D5C50B20096D530 /* DSPHRTFNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 83A349762D5C50B20096D530 /* DSPHRTFNode.m */; };
83B74281289E027F005AAC28 /* CogAudio-Bridging-Header.h in Headers */ = {isa = PBXBuildFile; fileRef = 83B74280289E027F005AAC28 /* CogAudio-Bridging-Header.h */; };
83F843202D5C6272008C123B /* HeadphoneFilter.h in Headers */ = {isa = PBXBuildFile; fileRef = 83F8431E2D5C6272008C123B /* HeadphoneFilter.h */; settings = {ATTRIBUTES = (Public, ); }; };
83F843212D5C6272008C123B /* HeadphoneFilter.mm in Sources */ = {isa = PBXBuildFile; fileRef = 83F8431F2D5C6272008C123B /* HeadphoneFilter.mm */; };
83F843232D5C66DA008C123B /* DSPEqualizerNode.h in Headers */ = {isa = PBXBuildFile; fileRef = 83F843222D5C66DA008C123B /* DSPEqualizerNode.h */; settings = {ATTRIBUTES = (Public, ); }; };
83F843252D5C66E9008C123B /* DSPEqualizerNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 83F843242D5C66E9008C123B /* DSPEqualizerNode.m */; };
83F9FFF62D6EC43900026576 /* soxr.h in Headers */ = {isa = PBXBuildFile; fileRef = 83F9FFF02D6EC43900026576 /* soxr.h */; settings = {ATTRIBUTES = (Public, ); }; };
83F9FFF82D6EC43900026576 /* libsoxr.0.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 83F9FFF22D6EC43900026576 /* libsoxr.0.dylib */; };
83FFED512D5B08BC0044CCAF /* DSPNode.h in Headers */ = {isa = PBXBuildFile; fileRef = 83FFED502D5B08BC0044CCAF /* DSPNode.h */; settings = {ATTRIBUTES = (Public, ); }; };
83FFED532D5B09320044CCAF /* DSPNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 83FFED522D5B09320044CCAF /* DSPNode.m */; };
8DC2EF570486A6940098B216 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7B1FEA5585E11CA2CBB /* Cocoa.framework */; };
8E8D3D2F0CBAEE6E00135C1B /* AudioContainer.h in Headers */ = {isa = PBXBuildFile; fileRef = 8E8D3D2D0CBAEE6E00135C1B /* AudioContainer.h */; settings = {ATTRIBUTES = (Public, ); }; };
8E8D3D300CBAEE6E00135C1B /* AudioContainer.m in Sources */ = {isa = PBXBuildFile; fileRef = 8E8D3D2E0CBAEE6E00135C1B /* AudioContainer.m */; };
8EC1225F0B993BD500C5B3AD /* ConverterNode.h in Headers */ = {isa = PBXBuildFile; fileRef = 8EC1225D0B993BD500C5B3AD /* ConverterNode.h */; };
8EC122600B993BD500C5B3AD /* ConverterNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = 8EC1225E0B993BD500C5B3AD /* ConverterNode.mm */; };
8EC1225F0B993BD500C5B3AD /* ConverterNode.h in Headers */ = {isa = PBXBuildFile; fileRef = 8EC1225D0B993BD500C5B3AD /* ConverterNode.h */; settings = {ATTRIBUTES = (Public, ); }; };
8EC122600B993BD500C5B3AD /* ConverterNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 8EC1225E0B993BD500C5B3AD /* ConverterNode.m */; };
B0575F2D0D687A0800411D77 /* Helper.h in Headers */ = {isa = PBXBuildFile; fileRef = B0575F2C0D687A0800411D77 /* Helper.h */; settings = {ATTRIBUTES = (Public, ); }; };
B0575F300D687A4000411D77 /* Helper.m in Sources */ = {isa = PBXBuildFile; fileRef = B0575F2F0D687A4000411D77 /* Helper.m */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
17D21D2B0B8BE6A200D1EBDE /* CopyFiles */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
83725A8D27AA0DDB0003F694 /* CopyFiles */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
@ -157,11 +152,9 @@
17D21C7D0B8BE4BA00D1EBDE /* Node.m */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.objc; path = Node.m; sourceTree = "<group>"; };
17D21C7E0B8BE4BA00D1EBDE /* OutputNode.h */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.h; path = OutputNode.h; sourceTree = "<group>"; };
17D21C7F0B8BE4BA00D1EBDE /* OutputNode.m */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.objc; path = OutputNode.m; sourceTree = "<group>"; };
17D21C9C0B8BE4BA00D1EBDE /* OutputCoreAudio.h */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.h; path = OutputCoreAudio.h; sourceTree = "<group>"; };
17D21C9D0B8BE4BA00D1EBDE /* OutputCoreAudio.m */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.objc; path = OutputCoreAudio.m; sourceTree = "<group>"; };
17D21C9E0B8BE4BA00D1EBDE /* Status.h */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.h; path = Status.h; sourceTree = "<group>"; };
17D21CF10B8BE5EF00D1EBDE /* Semaphore.h */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.h; path = Semaphore.h; sourceTree = "<group>"; };
17D21CF20B8BE5EF00D1EBDE /* Semaphore.m */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.objc; path = Semaphore.m; sourceTree = "<group>"; };
17D21CF10B8BE5EF00D1EBDE /* CogSemaphore.h */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.h; path = CogSemaphore.h; sourceTree = "<group>"; };
17D21CF20B8BE5EF00D1EBDE /* CogSemaphore.m */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.objc; path = CogSemaphore.m; sourceTree = "<group>"; };
17D21DA90B8BE76800D1EBDE /* AudioToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AudioToolbox.framework; path = /System/Library/Frameworks/AudioToolbox.framework; sourceTree = "<absolute>"; };
17D21DAA0B8BE76800D1EBDE /* AudioUnit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AudioUnit.framework; path = /System/Library/Frameworks/AudioUnit.framework; sourceTree = "<absolute>"; };
17D21DAB0B8BE76800D1EBDE /* CoreAudio.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreAudio.framework; path = /System/Library/Frameworks/CoreAudio.framework; sourceTree = "<absolute>"; };
@ -174,77 +167,85 @@
17F94DD40B8D0F7000A34E87 /* PluginController.mm */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.cpp.objcpp; path = PluginController.mm; sourceTree = "<group>"; };
17F94DDC0B8D101100A34E87 /* Plugin.h */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.h; path = Plugin.h; sourceTree = "<group>"; };
32DBCF5E0370ADEE00C91783 /* CogAudio_Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CogAudio_Prefix.pch; sourceTree = "<group>"; };
831A50132865A7FD0049CFE4 /* rsstate.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = rsstate.hpp; sourceTree = "<group>"; };
831A50152865A8800049CFE4 /* rsstate.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = rsstate.cpp; sourceTree = "<group>"; };
831A50172865A8B30049CFE4 /* rsstate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = rsstate.h; sourceTree = "<group>"; };
8328995127CB510F00D7F028 /* RedundantPlaylistDataStore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RedundantPlaylistDataStore.m; path = ../../Utils/RedundantPlaylistDataStore.m; sourceTree = "<group>"; };
8328995227CB511000D7F028 /* RedundantPlaylistDataStore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RedundantPlaylistDataStore.h; path = ../../Utils/RedundantPlaylistDataStore.h; sourceTree = "<group>"; };
8328995527CB51B700D7F028 /* SHA256Digest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SHA256Digest.h; path = ../../Utils/SHA256Digest.h; sourceTree = "<group>"; };
8328995627CB51B700D7F028 /* SHA256Digest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SHA256Digest.m; path = ../../Utils/SHA256Digest.m; sourceTree = "<group>"; };
8328995927CB51C900D7F028 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; };
833442402D6EFA6700C51D38 /* VisualizationController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VisualizationController.h; sourceTree = "<group>"; };
833442412D6EFA6700C51D38 /* VisualizationController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = VisualizationController.m; sourceTree = "<group>"; };
833738E92D5EA52500278628 /* DSPDownmixNode.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DSPDownmixNode.h; sourceTree = "<group>"; };
833738EB2D5EA53500278628 /* DSPDownmixNode.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DSPDownmixNode.m; sourceTree = "<group>"; };
833738ED2D5EA5B700278628 /* Downmix.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Downmix.h; sourceTree = "<group>"; };
833738EE2D5EA5B700278628 /* Downmix.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Downmix.m; sourceTree = "<group>"; };
8347C73F2796C58800FA8A7D /* NSFileHandle+CreateFile.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "NSFileHandle+CreateFile.h"; path = "../../Utils/NSFileHandle+CreateFile.h"; sourceTree = "<group>"; };
8347C7402796C58800FA8A7D /* NSFileHandle+CreateFile.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "NSFileHandle+CreateFile.m"; path = "../../Utils/NSFileHandle+CreateFile.m"; sourceTree = "<group>"; };
834A41A5287A90AB00EB9D9B /* freesurround_decoder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = freesurround_decoder.h; sourceTree = "<group>"; };
834A41A6287A90AB00EB9D9B /* freesurround_decoder.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = freesurround_decoder.cpp; sourceTree = "<group>"; };
834A41A7287A90AB00EB9D9B /* channelmaps.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = channelmaps.cpp; sourceTree = "<group>"; };
834A41A8287A90AB00EB9D9B /* channelmaps.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = channelmaps.h; sourceTree = "<group>"; };
834FD4EA27AF8F380063BC83 /* AudioChunk.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AudioChunk.h; sourceTree = "<group>"; };
834FD4EC27AF91220063BC83 /* AudioChunk.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AudioChunk.m; sourceTree = "<group>"; };
834FD4EE27AF93680063BC83 /* ChunkList.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ChunkList.h; sourceTree = "<group>"; };
834FD4EF27AF93680063BC83 /* ChunkList.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ChunkList.m; sourceTree = "<group>"; };
834FD4F227AFA2150063BC83 /* Downmix.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Downmix.h; sourceTree = "<group>"; };
834FD4F327AFA2150063BC83 /* Downmix.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Downmix.m; sourceTree = "<group>"; };
835C88A42797D4D400E28EAE /* LICENSE.LGPL */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE.LGPL; sourceTree = "<group>"; };
835C88A52797D4D400E28EAE /* License.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = License.txt; sourceTree = "<group>"; };
835C88A62797D4D400E28EAE /* lpc.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = lpc.c; sourceTree = "<group>"; };
835C88A72797D4D400E28EAE /* lpc.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = lpc.h; sourceTree = "<group>"; };
835C88AC2797DA5800E28EAE /* util.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = util.h; path = ThirdParty/lvqcl/util.h; sourceTree = SOURCE_ROOT; };
8350416C28646149006B32CC /* CoreMedia.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMedia.framework; path = System/Library/Frameworks/CoreMedia.framework; sourceTree = SDKROOT; };
835C88AF279811A500E28EAE /* hdcd_decode2.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = hdcd_decode2.h; sourceTree = "<group>"; };
835C88B0279811A500E28EAE /* hdcd_decode2.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = hdcd_decode2.c; sourceTree = "<group>"; };
835EDD7A279FE23A001EDCCE /* HeadphoneFilter.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = HeadphoneFilter.mm; sourceTree = "<group>"; };
835EDD7C279FE307001EDCCE /* HeadphoneFilter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HeadphoneFilter.h; sourceTree = "<group>"; };
835DD2652ACAF1D90057E319 /* OutputCoreAudio.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OutputCoreAudio.m; sourceTree = "<group>"; };
835DD2662ACAF1D90057E319 /* OutputCoreAudio.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OutputCoreAudio.h; sourceTree = "<group>"; };
835DD26B2ACAF5AD0057E319 /* LICENSE.LGPL */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE.LGPL; sourceTree = "<group>"; };
835DD26C2ACAF5AD0057E319 /* License.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = License.txt; sourceTree = "<group>"; };
835DD26D2ACAF5AD0057E319 /* lpc.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = lpc.h; sourceTree = "<group>"; };
835DD26E2ACAF5AD0057E319 /* util.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = util.h; sourceTree = "<group>"; };
835DD26F2ACAF5AD0057E319 /* lpc.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = lpc.c; sourceTree = "<group>"; };
835FAC5C27BCA14D00BA8562 /* BadSampleCleaner.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = BadSampleCleaner.h; path = Utils/BadSampleCleaner.h; sourceTree = SOURCE_ROOT; };
835FAC5D27BCA14D00BA8562 /* BadSampleCleaner.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = BadSampleCleaner.m; path = Utils/BadSampleCleaner.m; sourceTree = SOURCE_ROOT; };
8363BABD284E428F00E5C9DD /* pffft.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = pffft.cpp; sourceTree = "<group>"; };
83725A7B27AA0D8A0003F694 /* Accelerate.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Accelerate.framework; path = System/Library/Frameworks/Accelerate.framework; sourceTree = SDKROOT; };
83725A7C27AA0D8E0003F694 /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = SDKROOT; };
8377C64B27B8C51500E8BC0F /* fft_accelerate.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = fft_accelerate.c; sourceTree = "<group>"; };
8377C64D27B8C54400E8BC0F /* fft.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = fft.h; sourceTree = "<group>"; };
8377C65027B8CAD100E8BC0F /* VisualizationController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VisualizationController.h; sourceTree = "<group>"; };
8377C65127B8CAD100E8BC0F /* VisualizationController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = VisualizationController.m; sourceTree = "<group>"; };
8384912618080FF100E7332D /* Logging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Logging.h; path = ../../Utils/Logging.h; sourceTree = "<group>"; };
838A33712D06A97D00D0D770 /* librubberband.3.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = librubberband.3.dylib; path = ../ThirdParty/rubberband/lib/librubberband.3.dylib; sourceTree = SOURCE_ROOT; };
839065F22853338700636FBB /* dsd2float.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = dsd2float.h; sourceTree = "<group>"; };
839366651815923C006DD712 /* CogPluginMulti.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CogPluginMulti.h; sourceTree = "<group>"; };
839366661815923C006DD712 /* CogPluginMulti.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CogPluginMulti.m; sourceTree = "<group>"; };
8399CF2A27B5D1D4008751F1 /* NSDictionary+Merge.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "NSDictionary+Merge.h"; path = "../../Utils/NSDictionary+Merge.h"; sourceTree = "<group>"; };
8399CF2B27B5D1D4008751F1 /* NSDictionary+Merge.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "NSDictionary+Merge.m"; path = "../../Utils/NSDictionary+Merge.m"; sourceTree = "<group>"; };
83B69B6B2845DF6500D2435A /* pffft_double.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = pffft_double.h; sourceTree = "<group>"; };
83B69B6D2845DF6500D2435A /* pf_neon_double_from_avx.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = pf_neon_double_from_avx.h; sourceTree = "<group>"; };
83B69B6E2845DF6500D2435A /* pf_double.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = pf_double.h; sourceTree = "<group>"; };
83B69B6F2845DF6500D2435A /* pf_neon_double.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = pf_neon_double.h; sourceTree = "<group>"; };
83B69B702845DF6500D2435A /* pf_sse2_double.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = pf_sse2_double.h; sourceTree = "<group>"; };
83B69B712845DF6500D2435A /* pf_avx_double.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = pf_avx_double.h; sourceTree = "<group>"; };
83B69B722845DF6500D2435A /* pf_scalar_double.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = pf_scalar_double.h; sourceTree = "<group>"; };
83B69B732845DF6500D2435A /* pffft_priv_impl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = pffft_priv_impl.h; sourceTree = "<group>"; };
83B69B742845DF6500D2435A /* pffft_double.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = pffft_double.c; sourceTree = "<group>"; };
83F18ADF27D1E8EF00385946 /* CDSPHBDownsampler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CDSPHBDownsampler.h; sourceTree = "<group>"; };
83F18AF827D1E8EF00385946 /* CDSPSincFilterGen.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CDSPSincFilterGen.h; sourceTree = "<group>"; };
83F18AF927D1E8EF00385946 /* r8butil.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = r8butil.h; sourceTree = "<group>"; };
83F18AFB27D1E8EF00385946 /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
83F18AFC27D1E8EF00385946 /* r8bbase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = r8bbase.h; sourceTree = "<group>"; };
83F18AFD27D1E8EF00385946 /* CDSPFIRFilter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CDSPFIRFilter.h; sourceTree = "<group>"; };
83F18B0827D1E8EF00385946 /* CDSPProcessor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CDSPProcessor.h; sourceTree = "<group>"; };
83F18B0927D1E8EF00385946 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
83F18B0A27D1E8EF00385946 /* fft4g.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = fft4g.h; sourceTree = "<group>"; };
83F18B0B27D1E8EF00385946 /* CDSPRealFFT.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CDSPRealFFT.h; sourceTree = "<group>"; };
83F18B0C27D1E8EF00385946 /* CDSPFracInterpolator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CDSPFracInterpolator.h; sourceTree = "<group>"; };
83F18B1727D1E8EF00385946 /* CDSPBlockConvolver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CDSPBlockConvolver.h; sourceTree = "<group>"; };
83F18B1827D1E8EF00385946 /* CDSPHBUpsampler.inc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.pascal; path = CDSPHBUpsampler.inc; sourceTree = "<group>"; };
83F18B1927D1E8EF00385946 /* r8bconf.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = r8bconf.h; sourceTree = "<group>"; };
83F18B1A27D1E8EF00385946 /* r8bbase.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = r8bbase.cpp; sourceTree = "<group>"; };
83F18B1B27D1E8EF00385946 /* pffft.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = pffft.h; sourceTree = "<group>"; };
83F18B1C27D1E8EF00385946 /* CDSPResampler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CDSPResampler.h; sourceTree = "<group>"; };
83F18B1D27D1E8EF00385946 /* CDSPHBUpsampler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CDSPHBUpsampler.h; sourceTree = "<group>"; };
83F18B5527D1F5E900385946 /* r8bstate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = r8bstate.h; path = ThirdParty/r8bstate.h; sourceTree = SOURCE_ROOT; };
839E56E12879450300DFB5F4 /* HrtfData.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HrtfData.h; sourceTree = "<group>"; };
839E56E22879450300DFB5F4 /* Endianness.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Endianness.h; sourceTree = "<group>"; };
839E56E32879450300DFB5F4 /* HrtfData.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = HrtfData.cpp; sourceTree = "<group>"; };
839E56E42879450300DFB5F4 /* IHrtfData.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IHrtfData.h; sourceTree = "<group>"; };
839E56E928794F6300DFB5F4 /* HrtfTypes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HrtfTypes.h; sourceTree = "<group>"; };
839E56F6287974A100DFB5F4 /* SandboxBroker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SandboxBroker.h; path = ../Utils/SandboxBroker.h; sourceTree = "<group>"; };
839E899D2D5DB9D500A13526 /* VisualizationNode.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VisualizationNode.h; sourceTree = "<group>"; };
839E899F2D5DBA1700A13526 /* VisualizationNode.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = VisualizationNode.m; sourceTree = "<group>"; };
83A349672D5C3F430096D530 /* DSPRubberbandNode.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DSPRubberbandNode.h; sourceTree = "<group>"; };
83A349682D5C3F430096D530 /* DSPRubberbandNode.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DSPRubberbandNode.m; sourceTree = "<group>"; };
83A3496C2D5C40490096D530 /* DSPFSurroundNode.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DSPFSurroundNode.h; sourceTree = "<group>"; };
83A3496E2D5C405E0096D530 /* DSPFSurroundNode.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DSPFSurroundNode.m; sourceTree = "<group>"; };
83A349702D5C41810096D530 /* FSurroundFilter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSurroundFilter.h; sourceTree = "<group>"; };
83A349712D5C41810096D530 /* FSurroundFilter.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FSurroundFilter.mm; sourceTree = "<group>"; };
83A349742D5C50A10096D530 /* DSPHRTFNode.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DSPHRTFNode.h; sourceTree = "<group>"; };
83A349762D5C50B20096D530 /* DSPHRTFNode.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DSPHRTFNode.m; sourceTree = "<group>"; };
83B74280289E027F005AAC28 /* CogAudio-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "CogAudio-Bridging-Header.h"; sourceTree = "<group>"; };
83F8431E2D5C6272008C123B /* HeadphoneFilter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HeadphoneFilter.h; sourceTree = "<group>"; };
83F8431F2D5C6272008C123B /* HeadphoneFilter.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = HeadphoneFilter.mm; sourceTree = "<group>"; };
83F843222D5C66DA008C123B /* DSPEqualizerNode.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DSPEqualizerNode.h; sourceTree = "<group>"; };
83F843242D5C66E9008C123B /* DSPEqualizerNode.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DSPEqualizerNode.m; sourceTree = "<group>"; };
83F9FFF02D6EC43900026576 /* soxr.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = soxr.h; sourceTree = "<group>"; };
83F9FFF22D6EC43900026576 /* libsoxr.0.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; path = libsoxr.0.dylib; sourceTree = "<group>"; };
83F9FFF42D6EC43900026576 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
83FFED502D5B08BC0044CCAF /* DSPNode.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DSPNode.h; sourceTree = "<group>"; };
83FFED522D5B09320044CCAF /* DSPNode.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DSPNode.m; sourceTree = "<group>"; };
8DC2EF5A0486A6940098B216 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
8DC2EF5B0486A6940098B216 /* CogAudio.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CogAudio.framework; sourceTree = BUILT_PRODUCTS_DIR; };
8E8D3D2D0CBAEE6E00135C1B /* AudioContainer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AudioContainer.h; sourceTree = "<group>"; };
8E8D3D2E0CBAEE6E00135C1B /* AudioContainer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AudioContainer.m; sourceTree = "<group>"; };
8EC1225D0B993BD500C5B3AD /* ConverterNode.h */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.h; path = ConverterNode.h; sourceTree = "<group>"; };
8EC1225E0B993BD500C5B3AD /* ConverterNode.mm */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.cpp.objcpp; path = ConverterNode.mm; sourceTree = "<group>"; };
8EC1225E0B993BD500C5B3AD /* ConverterNode.m */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.objc; path = ConverterNode.m; sourceTree = "<group>"; };
B0575F2C0D687A0800411D77 /* Helper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Helper.h; sourceTree = "<group>"; };
B0575F2F0D687A4000411D77 /* Helper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Helper.m; sourceTree = "<group>"; };
D2F7E79907B2D74100F64583 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = /System/Library/Frameworks/CoreData.framework; sourceTree = "<absolute>"; };
@ -257,12 +258,15 @@
files = (
8328995A27CB51C900D7F028 /* Security.framework in Frameworks */,
83725A9127AA16D50003F694 /* AVFoundation.framework in Frameworks */,
83F9FFF82D6EC43900026576 /* libsoxr.0.dylib in Frameworks */,
8DC2EF570486A6940098B216 /* Cocoa.framework in Frameworks */,
8350416D28646149006B32CC /* CoreMedia.framework in Frameworks */,
83725A9027AA16C90003F694 /* Accelerate.framework in Frameworks */,
17D21DAD0B8BE76800D1EBDE /* AudioToolbox.framework in Frameworks */,
17D21DAE0B8BE76800D1EBDE /* AudioUnit.framework in Frameworks */,
17D21DAF0B8BE76800D1EBDE /* CoreAudio.framework in Frameworks */,
17D21DB00B8BE76800D1EBDE /* CoreAudioKit.framework in Frameworks */,
838A33722D06A97D00D0D770 /* librubberband.3.dylib in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -310,6 +314,7 @@
08FB77AEFE84172EC02AAC07 /* Classes */ = {
isa = PBXGroup;
children = (
83B74280289E027F005AAC28 /* CogAudio-Bridging-Header.h */,
8377C64F27B8CAAB00E8BC0F /* Visualization */,
17F94DDC0B8D101100A34E87 /* Plugin.h */,
17D21EBB0B8BF44000D1EBDE /* AudioPlayer.h */,
@ -332,6 +337,7 @@
17F94DD40B8D0F7000A34E87 /* PluginController.mm */,
17D21C750B8BE4BA00D1EBDE /* Chain */,
17D21C9B0B8BE4BA00D1EBDE /* Output */,
839E56F6287974A100DFB5F4 /* SandboxBroker.h */,
17D21C9E0B8BE4BA00D1EBDE /* Status.h */,
B0575F2C0D687A0800411D77 /* Helper.h */,
B0575F2F0D687A4000411D77 /* Helper.m */,
@ -352,6 +358,7 @@
1058C7B2FEA5585E11CA2CBB /* Other Frameworks */ = {
isa = PBXGroup;
children = (
838A33712D06A97D00D0D770 /* librubberband.3.dylib */,
83725A7B27AA0D8A0003F694 /* Accelerate.framework */,
17D21DAA0B8BE76800D1EBDE /* AudioUnit.framework */,
17D21DA90B8BE76800D1EBDE /* AudioToolbox.framework */,
@ -368,24 +375,25 @@
17D21C750B8BE4BA00D1EBDE /* Chain */ = {
isa = PBXGroup;
children = (
83A349692D5C3F430096D530 /* DSP */,
834FD4EA27AF8F380063BC83 /* AudioChunk.h */,
834FD4EC27AF91220063BC83 /* AudioChunk.m */,
834FD4EE27AF93680063BC83 /* ChunkList.h */,
834FD4EF27AF93680063BC83 /* ChunkList.m */,
834FD4F227AFA2150063BC83 /* Downmix.h */,
834FD4F327AFA2150063BC83 /* Downmix.m */,
17D21C760B8BE4BA00D1EBDE /* BufferChain.h */,
17D21C770B8BE4BA00D1EBDE /* BufferChain.m */,
834FD4EE27AF93680063BC83 /* ChunkList.h */,
834FD4EF27AF93680063BC83 /* ChunkList.m */,
8EC1225D0B993BD500C5B3AD /* ConverterNode.h */,
8EC1225E0B993BD500C5B3AD /* ConverterNode.mm */,
8EC1225E0B993BD500C5B3AD /* ConverterNode.m */,
17D21C7A0B8BE4BA00D1EBDE /* InputNode.h */,
17D21C7B0B8BE4BA00D1EBDE /* InputNode.m */,
17D21C7C0B8BE4BA00D1EBDE /* Node.h */,
17D21C7D0B8BE4BA00D1EBDE /* Node.m */,
17D21C7E0B8BE4BA00D1EBDE /* OutputNode.h */,
17D21C7F0B8BE4BA00D1EBDE /* OutputNode.m */,
835EDD7C279FE307001EDCCE /* HeadphoneFilter.h */,
835EDD7A279FE23A001EDCCE /* HeadphoneFilter.mm */,
83FFED502D5B08BC0044CCAF /* DSPNode.h */,
83FFED522D5B09320044CCAF /* DSPNode.m */,
839E899D2D5DB9D500A13526 /* VisualizationNode.h */,
839E899F2D5DBA1700A13526 /* VisualizationNode.m */,
);
path = Chain;
sourceTree = "<group>";
@ -393,8 +401,8 @@
17D21C9B0B8BE4BA00D1EBDE /* Output */ = {
isa = PBXGroup;
children = (
17D21C9C0B8BE4BA00D1EBDE /* OutputCoreAudio.h */,
17D21C9D0B8BE4BA00D1EBDE /* OutputCoreAudio.m */,
835DD2662ACAF1D90057E319 /* OutputCoreAudio.h */,
835DD2652ACAF1D90057E319 /* OutputCoreAudio.m */,
);
path = Output;
sourceTree = "<group>";
@ -402,10 +410,15 @@
17D21CD80B8BE5B400D1EBDE /* ThirdParty */ = {
isa = PBXGroup;
children = (
83F18ADE27D1E8EF00385946 /* r8brain-free-src */,
83F9FFF52D6EC43900026576 /* soxr */,
835DD2692ACAF5AD0057E319 /* lvqcl */,
834A41A4287A90AB00EB9D9B /* fsurround */,
839E56E02879450300DFB5F4 /* hrtf */,
831A50152865A8800049CFE4 /* rsstate.cpp */,
831A50172865A8B30049CFE4 /* rsstate.h */,
831A50132865A7FD0049CFE4 /* rsstate.hpp */,
8377C64A27B8C51500E8BC0F /* deadbeef */,
835C88AE279811A500E28EAE /* hdcd */,
835C88A22797D4D400E28EAE /* lvqcl */,
17D21DC40B8BE79700D1EBDE /* CoreAudioUtils */,
);
path = ThirdParty;
@ -426,8 +439,8 @@
8347C73F2796C58800FA8A7D /* NSFileHandle+CreateFile.h */,
8347C7402796C58800FA8A7D /* NSFileHandle+CreateFile.m */,
8384912618080FF100E7332D /* Logging.h */,
17D21CF10B8BE5EF00D1EBDE /* Semaphore.h */,
17D21CF20B8BE5EF00D1EBDE /* Semaphore.m */,
17D21CF10B8BE5EF00D1EBDE /* CogSemaphore.h */,
17D21CF20B8BE5EF00D1EBDE /* CogSemaphore.m */,
);
path = Utils;
sourceTree = "<group>";
@ -449,24 +462,15 @@
name = "Other Sources";
sourceTree = "<group>";
};
835C88A22797D4D400E28EAE /* lvqcl */ = {
834A41A4287A90AB00EB9D9B /* fsurround */ = {
isa = PBXGroup;
children = (
835C88A32797D4D400E28EAE /* License */,
835C88A62797D4D400E28EAE /* lpc.c */,
835C88A72797D4D400E28EAE /* lpc.h */,
835C88AC2797DA5800E28EAE /* util.h */,
834A41A5287A90AB00EB9D9B /* freesurround_decoder.h */,
834A41A6287A90AB00EB9D9B /* freesurround_decoder.cpp */,
834A41A7287A90AB00EB9D9B /* channelmaps.cpp */,
834A41A8287A90AB00EB9D9B /* channelmaps.h */,
);
path = lvqcl;
sourceTree = "<group>";
};
835C88A32797D4D400E28EAE /* License */ = {
isa = PBXGroup;
children = (
835C88A42797D4D400E28EAE /* LICENSE.LGPL */,
835C88A52797D4D400E28EAE /* License.txt */,
);
path = License;
path = fsurround;
sourceTree = "<group>";
};
835C88AE279811A500E28EAE /* hdcd */ = {
@ -478,9 +482,30 @@
path = hdcd;
sourceTree = "<group>";
};
835DD2692ACAF5AD0057E319 /* lvqcl */ = {
isa = PBXGroup;
children = (
835DD26A2ACAF5AD0057E319 /* License */,
835DD26D2ACAF5AD0057E319 /* lpc.h */,
835DD26E2ACAF5AD0057E319 /* util.h */,
835DD26F2ACAF5AD0057E319 /* lpc.c */,
);
path = lvqcl;
sourceTree = "<group>";
};
835DD26A2ACAF5AD0057E319 /* License */ = {
isa = PBXGroup;
children = (
835DD26B2ACAF5AD0057E319 /* LICENSE.LGPL */,
835DD26C2ACAF5AD0057E319 /* License.txt */,
);
path = License;
sourceTree = "<group>";
};
83725A8F27AA16C90003F694 /* Frameworks */ = {
isa = PBXGroup;
children = (
8350416C28646149006B32CC /* CoreMedia.framework */,
8328995927CB51C900D7F028 /* Security.framework */,
);
name = Frameworks;
@ -498,64 +523,74 @@
8377C64F27B8CAAB00E8BC0F /* Visualization */ = {
isa = PBXGroup;
children = (
8377C65027B8CAD100E8BC0F /* VisualizationController.h */,
8377C65127B8CAD100E8BC0F /* VisualizationController.m */,
833442402D6EFA6700C51D38 /* VisualizationController.h */,
833442412D6EFA6700C51D38 /* VisualizationController.m */,
);
path = Visualization;
sourceTree = "<group>";
};
83B69B6A2845DF6500D2435A /* pffft_double */ = {
839E56E02879450300DFB5F4 /* hrtf */ = {
isa = PBXGroup;
children = (
83B69B6B2845DF6500D2435A /* pffft_double.h */,
83B69B6C2845DF6500D2435A /* simd */,
83B69B732845DF6500D2435A /* pffft_priv_impl.h */,
83B69B742845DF6500D2435A /* pffft_double.c */,
839E56E22879450300DFB5F4 /* Endianness.h */,
839E56E32879450300DFB5F4 /* HrtfData.cpp */,
839E56E12879450300DFB5F4 /* HrtfData.h */,
839E56E928794F6300DFB5F4 /* HrtfTypes.h */,
839E56E42879450300DFB5F4 /* IHrtfData.h */,
);
path = pffft_double;
path = hrtf;
sourceTree = "<group>";
};
83B69B6C2845DF6500D2435A /* simd */ = {
83A349692D5C3F430096D530 /* DSP */ = {
isa = PBXGroup;
children = (
83B69B6D2845DF6500D2435A /* pf_neon_double_from_avx.h */,
83B69B6E2845DF6500D2435A /* pf_double.h */,
83B69B6F2845DF6500D2435A /* pf_neon_double.h */,
83B69B702845DF6500D2435A /* pf_sse2_double.h */,
83B69B712845DF6500D2435A /* pf_avx_double.h */,
83B69B722845DF6500D2435A /* pf_scalar_double.h */,
833738ED2D5EA5B700278628 /* Downmix.h */,
833738EE2D5EA5B700278628 /* Downmix.m */,
83F8431E2D5C6272008C123B /* HeadphoneFilter.h */,
83F8431F2D5C6272008C123B /* HeadphoneFilter.mm */,
83A349702D5C41810096D530 /* FSurroundFilter.h */,
83A349712D5C41810096D530 /* FSurroundFilter.mm */,
83A349672D5C3F430096D530 /* DSPRubberbandNode.h */,
83A349682D5C3F430096D530 /* DSPRubberbandNode.m */,
83A3496C2D5C40490096D530 /* DSPFSurroundNode.h */,
83A3496E2D5C405E0096D530 /* DSPFSurroundNode.m */,
83A349742D5C50A10096D530 /* DSPHRTFNode.h */,
83A349762D5C50B20096D530 /* DSPHRTFNode.m */,
83F843222D5C66DA008C123B /* DSPEqualizerNode.h */,
83F843242D5C66E9008C123B /* DSPEqualizerNode.m */,
833738E92D5EA52500278628 /* DSPDownmixNode.h */,
833738EB2D5EA53500278628 /* DSPDownmixNode.m */,
);
path = simd;
path = DSP;
sourceTree = "<group>";
};
83F18ADE27D1E8EF00385946 /* r8brain-free-src */ = {
83F9FFF12D6EC43900026576 /* include */ = {
isa = PBXGroup;
children = (
8363BABD284E428F00E5C9DD /* pffft.cpp */,
83B69B6A2845DF6500D2435A /* pffft_double */,
83F18ADF27D1E8EF00385946 /* CDSPHBDownsampler.h */,
83F18AF827D1E8EF00385946 /* CDSPSincFilterGen.h */,
83F18AF927D1E8EF00385946 /* r8butil.h */,
83F18AFB27D1E8EF00385946 /* LICENSE */,
83F18AFC27D1E8EF00385946 /* r8bbase.h */,
83F18AFD27D1E8EF00385946 /* CDSPFIRFilter.h */,
83F18B0827D1E8EF00385946 /* CDSPProcessor.h */,
83F18B0927D1E8EF00385946 /* README.md */,
83F18B0A27D1E8EF00385946 /* fft4g.h */,
83F18B0B27D1E8EF00385946 /* CDSPRealFFT.h */,
83F18B0C27D1E8EF00385946 /* CDSPFracInterpolator.h */,
83F18B1727D1E8EF00385946 /* CDSPBlockConvolver.h */,
83F18B1827D1E8EF00385946 /* CDSPHBUpsampler.inc */,
83F18B1927D1E8EF00385946 /* r8bconf.h */,
83F18B1A27D1E8EF00385946 /* r8bbase.cpp */,
83F18B1B27D1E8EF00385946 /* pffft.h */,
83F18B1C27D1E8EF00385946 /* CDSPResampler.h */,
83F18B1D27D1E8EF00385946 /* CDSPHBUpsampler.h */,
83F18B5527D1F5E900385946 /* r8bstate.h */,
83F9FFF02D6EC43900026576 /* soxr.h */,
);
path = "r8brain-free-src";
path = include;
sourceTree = "<group>";
};
83F9FFF32D6EC43900026576 /* lib */ = {
isa = PBXGroup;
children = (
83F9FFF22D6EC43900026576 /* libsoxr.0.dylib */,
);
path = lib;
sourceTree = "<group>";
};
83F9FFF52D6EC43900026576 /* soxr */ = {
isa = PBXGroup;
children = (
83F9FFF12D6EC43900026576 /* include */,
83F9FFF32D6EC43900026576 /* lib */,
83F9FFF42D6EC43900026576 /* README.md */,
);
name = soxr;
path = ../ThirdParty/soxr;
sourceTree = SOURCE_ROOT;
};
/* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */
@ -563,76 +598,71 @@
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
files = (
83B69B7C2845DF6500D2435A /* pffft_priv_impl.h in Headers */,
833442422D6EFA6700C51D38 /* VisualizationController.h in Headers */,
833738F02D5EA5B700278628 /* Downmix.h in Headers */,
834FD4EB27AF8F380063BC83 /* AudioChunk.h in Headers */,
83F843202D5C6272008C123B /* HeadphoneFilter.h in Headers */,
83A349732D5C41810096D530 /* FSurroundFilter.h in Headers */,
839E56E82879450300DFB5F4 /* IHrtfData.h in Headers */,
17D21CA10B8BE4BA00D1EBDE /* BufferChain.h in Headers */,
831A50142865A7FD0049CFE4 /* rsstate.hpp in Headers */,
835DD2682ACAF1D90057E319 /* OutputCoreAudio.h in Headers */,
834A41AC287A90AB00EB9D9B /* channelmaps.h in Headers */,
83A3496D2D5C40490096D530 /* DSPFSurroundNode.h in Headers */,
83A3496B2D5C3F430096D530 /* DSPRubberbandNode.h in Headers */,
17D21CA50B8BE4BA00D1EBDE /* InputNode.h in Headers */,
833738EA2D5EA52500278628 /* DSPDownmixNode.h in Headers */,
83F843232D5C66DA008C123B /* DSPEqualizerNode.h in Headers */,
834A41A9287A90AB00EB9D9B /* freesurround_decoder.h in Headers */,
834FD4F027AF93680063BC83 /* ChunkList.h in Headers */,
835DD2732ACAF5AD0057E319 /* util.h in Headers */,
17D21CA70B8BE4BA00D1EBDE /* Node.h in Headers */,
83B69B7B2845DF6500D2435A /* pf_scalar_double.h in Headers */,
83F18B3427D1E8EF00385946 /* r8butil.h in Headers */,
8399CF2C27B5D1D5008751F1 /* NSDictionary+Merge.h in Headers */,
17D21CA90B8BE4BA00D1EBDE /* OutputNode.h in Headers */,
83B69B792845DF6500D2435A /* pf_sse2_double.h in Headers */,
83F18B4527D1E8EF00385946 /* CDSPRealFFT.h in Headers */,
83F18B3827D1E8EF00385946 /* CDSPFIRFilter.h in Headers */,
8EC1225F0B993BD500C5B3AD /* ConverterNode.h in Headers */,
8328995427CB511000D7F028 /* RedundantPlaylistDataStore.h in Headers */,
17D21CC50B8BE4BA00D1EBDE /* OutputCoreAudio.h in Headers */,
834FD4F427AFA2150063BC83 /* Downmix.h in Headers */,
839E56E52879450300DFB5F4 /* HrtfData.h in Headers */,
83FFED512D5B08BC0044CCAF /* DSPNode.h in Headers */,
839E899E2D5DB9D500A13526 /* VisualizationNode.h in Headers */,
83A349752D5C50A10096D530 /* DSPHRTFNode.h in Headers */,
83F9FFF62D6EC43900026576 /* soxr.h in Headers */,
17D21CC70B8BE4BA00D1EBDE /* Status.h in Headers */,
835C88AB2797D4D400E28EAE /* lpc.h in Headers */,
17D21CF30B8BE5EF00D1EBDE /* Semaphore.h in Headers */,
17D21CF30B8BE5EF00D1EBDE /* CogSemaphore.h in Headers */,
839E56E62879450300DFB5F4 /* Endianness.h in Headers */,
17D21DC70B8BE79700D1EBDE /* CoreAudioUtils.h in Headers */,
83F18B3327D1E8EF00385946 /* CDSPSincFilterGen.h in Headers */,
835DD2722ACAF5AD0057E319 /* lpc.h in Headers */,
17D21EBD0B8BF44000D1EBDE /* AudioPlayer.h in Headers */,
83F18B4427D1E8EF00385946 /* fft4g.h in Headers */,
83F18B1E27D1E8EF00385946 /* CDSPHBDownsampler.h in Headers */,
83B69B762845DF6500D2435A /* pf_neon_double_from_avx.h in Headers */,
83F18B5627D1F5E900385946 /* r8bstate.h in Headers */,
8377C65227B8CAD100E8BC0F /* VisualizationController.h in Headers */,
83B69B772845DF6500D2435A /* pf_double.h in Headers */,
834FD4F027AF93680063BC83 /* ChunkList.h in Headers */,
831A50182865A8B30049CFE4 /* rsstate.h in Headers */,
17F94DD50B8D0F7000A34E87 /* PluginController.h in Headers */,
83B69B782845DF6500D2435A /* pf_neon_double.h in Headers */,
17F94DDD0B8D101100A34E87 /* Plugin.h in Headers */,
83F18B5227D1E8F000385946 /* pffft.h in Headers */,
8328995727CB51B700D7F028 /* SHA256Digest.h in Headers */,
83B69B752845DF6500D2435A /* pffft_double.h in Headers */,
83F18B4627D1E8EF00385946 /* CDSPFracInterpolator.h in Headers */,
834FD4EB27AF8F380063BC83 /* AudioChunk.h in Headers */,
83F18B4E27D1E8F000385946 /* CDSPBlockConvolver.h in Headers */,
83F18B4227D1E8EF00385946 /* CDSPProcessor.h in Headers */,
83B69B7A2845DF6500D2435A /* pf_avx_double.h in Headers */,
17A2D3C50B8D1D37000778C4 /* AudioDecoder.h in Headers */,
8347C7412796C58800FA8A7D /* NSFileHandle+CreateFile.h in Headers */,
83B74281289E027F005AAC28 /* CogAudio-Bridging-Header.h in Headers */,
17C940230B900909008627D6 /* AudioMetadataReader.h in Headers */,
839E56F7287974A100DFB5F4 /* SandboxBroker.h in Headers */,
839065F32853338700636FBB /* dsd2float.h in Headers */,
17B619300B909BC300BC003F /* AudioPropertiesReader.h in Headers */,
83F18B5427D1E8F000385946 /* CDSPHBUpsampler.h in Headers */,
835EDD7D279FE307001EDCCE /* HeadphoneFilter.h in Headers */,
839366671815923C006DD712 /* CogPluginMulti.h in Headers */,
83F18B5327D1E8F000385946 /* CDSPResampler.h in Headers */,
17ADB13C0B97926D00257CA2 /* AudioSource.h in Headers */,
835C88B1279811A500E28EAE /* hdcd_decode2.h in Headers */,
8EC1225F0B993BD500C5B3AD /* ConverterNode.h in Headers */,
8384912718080FF100E7332D /* Logging.h in Headers */,
8377C64E27B8C54400E8BC0F /* fft.h in Headers */,
83F18B5027D1E8F000385946 /* r8bconf.h in Headers */,
835FAC5E27BCA14D00BA8562 /* BadSampleCleaner.h in Headers */,
8E8D3D2F0CBAEE6E00135C1B /* AudioContainer.h in Headers */,
83F18B3727D1E8EF00385946 /* r8bbase.h in Headers */,
B0575F2D0D687A0800411D77 /* Helper.h in Headers */,
835C88AD2797DA5800E28EAE /* util.h in Headers */,
07DB5F3E0ED353A900C2E3EF /* AudioMetadataWriter.h in Headers */,
839E56EA28794F6300DFB5F4 /* HrtfTypes.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXHeadersBuildPhase section */
/* Begin PBXNativeTarget section */
8DC2EF4F0486A6940098B216 /* CogAudio Framework */ = {
8DC2EF4F0486A6940098B216 /* CogAudio */ = {
isa = PBXNativeTarget;
buildConfigurationList = 1DEB91AD08733DA50010E9CD /* Build configuration list for PBXNativeTarget "CogAudio Framework" */;
buildConfigurationList = 1DEB91AD08733DA50010E9CD /* Build configuration list for PBXNativeTarget "CogAudio" */;
buildPhases = (
17D21D2B0B8BE6A200D1EBDE /* CopyFiles */,
8DC2EF500486A6940098B216 /* Headers */,
8DC2EF540486A6940098B216 /* Sources */,
8DC2EF560486A6940098B216 /* Frameworks */,
@ -643,7 +673,7 @@
);
dependencies = (
);
name = "CogAudio Framework";
name = CogAudio;
productInstallPath = "$(HOME)/Library/Frameworks";
productName = CogAudio;
productReference = 8DC2EF5B0486A6940098B216 /* CogAudio.framework */;
@ -655,11 +685,12 @@
0867D690FE84028FC02AAC07 /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 1400;
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1620;
TargetAttributes = {
8DC2EF4F0486A6940098B216 = {
DevelopmentTeam = "";
ProvisioningStyle = Automatic;
LastSwiftMigration = 1330;
ProvisioningStyle = Manual;
};
};
};
@ -676,7 +707,7 @@
projectDirPath = "";
projectRoot = "";
targets = (
8DC2EF4F0486A6940098B216 /* CogAudio Framework */,
8DC2EF4F0486A6940098B216 /* CogAudio */,
);
};
/* End PBXProject section */
@ -686,10 +717,6 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
83F18B3627D1E8EF00385946 /* LICENSE in Resources */,
835C88A92797D4D400E28EAE /* License.txt in Resources */,
835C88A82797D4D400E28EAE /* LICENSE.LGPL in Resources */,
83F18B4327D1E8EF00385946 /* README.md in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -700,39 +727,48 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
83D40B572852CB3B003BB85C /* pffft_double.c in Sources */,
835EDD7B279FE23A001EDCCE /* HeadphoneFilter.mm in Sources */,
17D21CA20B8BE4BA00D1EBDE /* BufferChain.m in Sources */,
83A349772D5C50B20096D530 /* DSPHRTFNode.m in Sources */,
17D21CA60B8BE4BA00D1EBDE /* InputNode.m in Sources */,
83A3496A2D5C3F430096D530 /* DSPRubberbandNode.m in Sources */,
8399CF2D27B5D1D5008751F1 /* NSDictionary+Merge.m in Sources */,
83F843252D5C66E9008C123B /* DSPEqualizerNode.m in Sources */,
834A41AB287A90AB00EB9D9B /* channelmaps.cpp in Sources */,
833738EC2D5EA53500278628 /* DSPDownmixNode.m in Sources */,
833442432D6EFA6700C51D38 /* VisualizationController.m in Sources */,
831A50162865A8800049CFE4 /* rsstate.cpp in Sources */,
17D21CA80B8BE4BA00D1EBDE /* Node.m in Sources */,
17D21CAA0B8BE4BA00D1EBDE /* OutputNode.m in Sources */,
8377C65327B8CAD100E8BC0F /* VisualizationController.m in Sources */,
834FD4F527AFA2150063BC83 /* Downmix.m in Sources */,
17D21CC60B8BE4BA00D1EBDE /* OutputCoreAudio.m in Sources */,
835C88B2279811A500E28EAE /* hdcd_decode2.c in Sources */,
835FAC5F27BCA14D00BA8562 /* BadSampleCleaner.m in Sources */,
834FD4ED27AF91220063BC83 /* AudioChunk.m in Sources */,
83F18B5127D1E8F000385946 /* r8bbase.cpp in Sources */,
17D21CF40B8BE5EF00D1EBDE /* Semaphore.m in Sources */,
833738EF2D5EA5B700278628 /* Downmix.m in Sources */,
17D21CF40B8BE5EF00D1EBDE /* CogSemaphore.m in Sources */,
839E89A02D5DBA1700A13526 /* VisualizationNode.m in Sources */,
8347C7422796C58800FA8A7D /* NSFileHandle+CreateFile.m in Sources */,
83A3496F2D5C405E0096D530 /* DSPFSurroundNode.m in Sources */,
17D21DC80B8BE79700D1EBDE /* CoreAudioUtils.m in Sources */,
8328995327CB511000D7F028 /* RedundantPlaylistDataStore.m in Sources */,
8377C64C27B8C51500E8BC0F /* fft_accelerate.c in Sources */,
8363BABE284E428F00E5C9DD /* pffft.cpp in Sources */,
839366681815923C006DD712 /* CogPluginMulti.m in Sources */,
835C88AA2797D4D400E28EAE /* lpc.c in Sources */,
17D21EBE0B8BF44000D1EBDE /* AudioPlayer.m in Sources */,
17F94DD60B8D0F7000A34E87 /* PluginController.mm in Sources */,
839E56E72879450300DFB5F4 /* HrtfData.cpp in Sources */,
17A2D3C60B8D1D37000778C4 /* AudioDecoder.m in Sources */,
8328995827CB51B700D7F028 /* SHA256Digest.m in Sources */,
17C940240B900909008627D6 /* AudioMetadataReader.m in Sources */,
17B619310B909BC300BC003F /* AudioPropertiesReader.m in Sources */,
83F843212D5C6272008C123B /* HeadphoneFilter.mm in Sources */,
17ADB13D0B97926D00257CA2 /* AudioSource.m in Sources */,
834FD4F127AF93680063BC83 /* ChunkList.m in Sources */,
8EC122600B993BD500C5B3AD /* ConverterNode.mm in Sources */,
83FFED532D5B09320044CCAF /* DSPNode.m in Sources */,
8EC122600B993BD500C5B3AD /* ConverterNode.m in Sources */,
835DD2672ACAF1D90057E319 /* OutputCoreAudio.m in Sources */,
83A349722D5C41810096D530 /* FSurroundFilter.mm in Sources */,
8E8D3D300CBAEE6E00135C1B /* AudioContainer.m in Sources */,
B0575F300D687A4000411D77 /* Helper.m in Sources */,
835DD2742ACAF5AD0057E319 /* lpc.c in Sources */,
834A41AA287A90AB00EB9D9B /* freesurround_decoder.cpp in Sources */,
07DB5F3F0ED353A900C2E3EF /* AudioMetadataWriter.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -743,69 +779,76 @@
1DEB91AE08733DA50010E9CD /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_IDENTITY = "";
CODE_SIGN_STYLE = Automatic;
CLANG_CXX_LANGUAGE_STANDARD = "c++17";
CLANG_ENABLE_MODULES = YES;
COMBINE_HIDPI_IMAGES = YES;
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = "";
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
ENABLE_MODULE_VERIFIER = YES;
FRAMEWORK_VERSION = A;
GCC_DYNAMIC_NO_PIC = NO;
GCC_ENABLE_OBJC_EXCEPTIONS = YES;
GCC_MODEL_TUNING = G5;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PRECOMPILE_PREFIX_HEADER = YES;
GCC_PREFIX_HEADER = CogAudio_Prefix.pch;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"R8B_EXTFFT=1",
"R8B_PFFFT_DOUBLE=1",
GCC_PREPROCESSOR_DEFINITIONS = "DEBUG=1";
HEADER_SEARCH_PATHS = (
../ThirdParty/soxr/include,
../ThirdParty/rubberband/include,
);
INFOPLIST_FILE = Info.plist;
INSTALL_PATH = "@executable_path/../Frameworks";
LD_RUNPATH_SEARCH_PATHS = "@loader_path/Frameworks";
LIBRARY_SEARCH_PATHS = (
../ThirdParty/soxr/lib,
../ThirdParty/rubberband/lib,
);
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 c++17";
OTHER_LDFLAGS = "";
PRODUCT_BUNDLE_IDENTIFIER = org.cogx.cogaudio;
PRODUCT_NAME = CogAudio;
PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = macosx;
SKIP_INSTALL = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
WARNING_LDFLAGS = "";
WRAPPER_EXTENSION = framework;
ZERO_LINK = YES;
};
name = Debug;
};
1DEB91AF08733DA50010E9CD /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_IDENTITY = "";
CODE_SIGN_STYLE = Automatic;
CLANG_CXX_LANGUAGE_STANDARD = "c++17";
CLANG_ENABLE_MODULES = YES;
COMBINE_HIDPI_IMAGES = YES;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = "";
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
ENABLE_MODULE_VERIFIER = YES;
FRAMEWORK_VERSION = A;
GCC_ENABLE_OBJC_EXCEPTIONS = YES;
GCC_MODEL_TUNING = G5;
GCC_PRECOMPILE_PREFIX_HEADER = YES;
GCC_PREFIX_HEADER = CogAudio_Prefix.pch;
GCC_PREPROCESSOR_DEFINITIONS = (
"R8B_EXTFFT=1",
"R8B_PFFFT_DOUBLE=1",
GCC_PREPROCESSOR_DEFINITIONS = "";
HEADER_SEARCH_PATHS = (
../ThirdParty/soxr/include,
../ThirdParty/rubberband/include,
);
INFOPLIST_FILE = Info.plist;
INSTALL_PATH = "@executable_path/../Frameworks";
LD_RUNPATH_SEARCH_PATHS = "@loader_path/Frameworks";
LIBRARY_SEARCH_PATHS = (
../ThirdParty/soxr/lib,
../ThirdParty/rubberband/lib,
);
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 c++17";
OTHER_LDFLAGS = "";
PRODUCT_BUNDLE_IDENTIFIER = org.cogx.cogaudio;
PRODUCT_NAME = CogAudio;
PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = macosx;
SKIP_INSTALL = YES;
SWIFT_VERSION = 5.0;
WARNING_LDFLAGS = "";
WRAPPER_EXTENSION = framework;
};
@ -816,6 +859,7 @@
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
@ -837,8 +881,10 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEFINES_MODULE = YES;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_NO_COMMON_BLOCKS = YES;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
@ -850,9 +896,13 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.12;
MACOSX_DEPLOYMENT_TARGET = 10.13;
ONLY_ACTIVE_ARCH = YES;
OTHER_CFLAGS = "-Wframe-larger-than=4000";
OTHER_CPLUSPLUSFLAGS = "-Wframe-larger-than=16000";
PRODUCT_MODULE_NAME = CogAudio;
SDKROOT = macosx;
SWIFT_OBJC_BRIDGING_HEADER = "CogAudio-Bridging-Header.h";
SYMROOT = ../build;
};
name = Debug;
@ -862,6 +912,7 @@
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
@ -883,7 +934,9 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEFINES_MODULE = YES;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_NO_COMMON_BLOCKS = YES;
GCC_PREPROCESSOR_DEFINITIONS = "HAVE_CONFIG_H=1";
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
@ -892,8 +945,13 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.12;
MACOSX_DEPLOYMENT_TARGET = 10.13;
OTHER_CFLAGS = "-Wframe-larger-than=4000";
OTHER_CPLUSPLUSFLAGS = "-Wframe-larger-than=16000";
PRODUCT_MODULE_NAME = CogAudio;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OBJC_BRIDGING_HEADER = "CogAudio-Bridging-Header.h";
SYMROOT = ../build;
};
name = Release;
@ -901,7 +959,7 @@
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
1DEB91AD08733DA50010E9CD /* Build configuration list for PBXNativeTarget "CogAudio Framework" */ = {
1DEB91AD08733DA50010E9CD /* Build configuration list for PBXNativeTarget "CogAudio" */ = {
isa = XCConfigurationList;
buildConfigurations = (
1DEB91AE08733DA50010E9CD /* Debug */,

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1400"
LastUpgradeVersion = "1620"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@ -16,7 +16,7 @@
BuildableIdentifier = "primary"
BlueprintIdentifier = "8DC2EF4F0486A6940098B216"
BuildableName = "CogAudio.framework"
BlueprintName = "CogAudio Framework"
BlueprintName = "CogAudio"
ReferencedContainer = "container:CogAudio.xcodeproj">
</BuildableReference>
</BuildActionEntry>
@ -45,7 +45,7 @@
BuildableIdentifier = "primary"
BlueprintIdentifier = "8DC2EF4F0486A6940098B216"
BuildableName = "CogAudio.framework"
BlueprintName = "CogAudio Framework"
BlueprintName = "CogAudio"
ReferencedContainer = "container:CogAudio.xcodeproj">
</BuildableReference>
</MacroExpansion>
@ -61,7 +61,7 @@
BuildableIdentifier = "primary"
BlueprintIdentifier = "8DC2EF4F0486A6940098B216"
BuildableName = "CogAudio.framework"
BlueprintName = "CogAudio Framework"
BlueprintName = "CogAudio"
ReferencedContainer = "container:CogAudio.xcodeproj">
</BuildableReference>
</MacroExpansion>

View file

@ -23,6 +23,7 @@
}
+ (NSArray *)urlsForContainerURL:(NSURL *)url containers:(NSArray *)containers;
+ (NSArray *)dependencyUrlsForContainerURL:(NSURL *)url containers:(NSArray *)containers;
@end

View file

@ -79,9 +79,9 @@ static void *kCogDecoderMultiContext = &kCogDecoderMultiContext;
return @{};
}
- (int)readAudio:(void *)buffer frames:(UInt32)frames {
if(theDecoder != nil) return [theDecoder readAudio:buffer frames:frames];
return 0;
- (AudioChunk *)readAudio {
if(theDecoder != nil) return [theDecoder readAudio];
return nil;
}
- (BOOL)open:(id<CogSource>)source {
@ -171,6 +171,19 @@ static void *kCogDecoderMultiContext = &kCogDecoderMultiContext;
return nil;
}
+ (NSArray *)dependencyUrlsForContainerURL:(NSURL *)url containers:(NSArray *)containers {
NSArray *sortedContainers = sortClassesByPriority(containers);
for(NSString *classString in sortedContainers) {
Class container = NSClassFromString(classString);
if([container respondsToSelector:@selector(dependencyUrlsForContainerURL:)]) {
NSArray *urls = [container dependencyUrlsForContainerURL:url];
if([urls count])
return urls;
}
}
return nil;
}
@end
@implementation CogMetadataReaderMulti

View file

@ -7,5 +7,5 @@
*
*/
double logarithmicToLinear(double logarithmic, double MAX_VOLUME);
double linearToLogarithmic(double linear, double MAX_VOLUME);
double logarithmicToLinear(const double logarithmic, double MAX_VOLUME);
double linearToLogarithmic(const double linear, double MAX_VOLUME);

View file

@ -13,13 +13,13 @@
// These functions are helpers for the process of converting volume from a linear to logarithmic scale.
// Numbers that goes in to audioPlayer should be logarithmic. Numbers that are displayed to the user should be linear.
// Here's why: http://www.dr-lex.34sp.com/info-stuff/volumecontrols.html
// We are using the approximation of X^4.
// We are using the approximation of X^2 when volume is limited to 100% and X^4 when volume is limited to 800%.
// Input/Output values are in percents.
double logarithmicToLinear(double logarithmic, double MAX_VOLUME) {
return (MAX_VOLUME == 100.0) ? logarithmic : pow((logarithmic / MAX_VOLUME), 0.25) * 100.0;
double logarithmicToLinear(const double logarithmic, double MAX_VOLUME) {
return (MAX_VOLUME == 100.0) ? pow((logarithmic / MAX_VOLUME), 0.5) * 100.0 : pow((logarithmic / MAX_VOLUME), 0.25) * 100.0;
}
double linearToLogarithmic(double linear, double MAX_VOLUME) {
return (MAX_VOLUME == 100.0) ? linear : (linear / 100.0) * (linear / 100.0) * (linear / 100.0) * (linear / 100.0) * MAX_VOLUME;
double linearToLogarithmic(const double linear, double MAX_VOLUME) {
return (MAX_VOLUME == 100.0) ? (linear / 100.0) * (linear / 100.0) * MAX_VOLUME : (linear / 100.0) * (linear / 100.0) * (linear / 100.0) * (linear / 100.0) * MAX_VOLUME;
}
// End helper volume function thingies. ONWARDS TO GLORY!

View file

@ -0,0 +1,160 @@
//
// OutputAVFoundation.h
// Cog
//
// Created by Christopher Snowhill on 6/23/22.
// Copyright 2022 Christopher Snowhill. All rights reserved.
//
#import <AssertMacros.h>
#import <Cocoa/Cocoa.h>
#import <AVFoundation/AVFoundation.h>
#import <AudioToolbox/AudioToolbox.h>
#import <AudioUnit/AudioUnit.h>
#import <CoreAudio/AudioHardware.h>
#import <CoreAudio/CoreAudioTypes.h>
#ifdef __cplusplus
#import <atomic>
using std::atomic_long;
#else
#import <stdatomic.h>
#endif
#import "Downmix.h"
#import <CogAudio/CogAudio-Swift.h>
#import "HeadphoneFilter.h"
//#define OUTPUT_LOG
#ifdef OUTPUT_LOG
#import <stdio.h>
#endif
@class OutputNode;
@class FSurroundFilter;
@interface OutputAVFoundation : NSObject {
OutputNode *outputController;
BOOL rsDone;
void *rsstate, *rsold;
double lastClippedSampleRate;
void *rsvis;
double lastVisRate;
BOOL stopInvoked;
BOOL stopCompleted;
BOOL running;
BOOL stopping;
BOOL stopped;
BOOL started;
BOOL paused;
BOOL restarted;
BOOL commandStop;
BOOL eqEnabled;
BOOL eqInitialized;
BOOL streamFormatStarted;
BOOL streamFormatChanged;
double secondsHdcdSustained;
BOOL defaultdevicelistenerapplied;
BOOL currentdevicelistenerapplied;
BOOL devicealivelistenerapplied;
BOOL observersapplied;
BOOL outputdevicechanged;
float volume;
float eqPreamp;
AudioDeviceID outputDeviceID;
AudioStreamBasicDescription realStreamFormat; // stream format pre-hrtf
AudioStreamBasicDescription streamFormat; // stream format last seen in render callback
AudioStreamBasicDescription realNewFormat; // in case of resampler flush
AudioStreamBasicDescription newFormat; // in case of resampler flush
AudioStreamBasicDescription visFormat; // Mono format for vis
uint32_t realStreamChannelConfig;
uint32_t streamChannelConfig;
uint32_t realNewChannelConfig;
uint32_t newChannelConfig;
AVSampleBufferAudioRenderer *audioRenderer;
AVSampleBufferRenderSynchronizer *renderSynchronizer;
CMAudioFormatDescriptionRef audioFormatDescription;
id currentPtsObserver;
NSLock *currentPtsLock;
CMTime currentPts, lastPts;
double secondsLatency;
CMTime outputPts, trackPts, lastCheckpointPts;
AudioTimeStamp timeStamp;
size_t _bufferSize;
AudioUnit _eq;
DownmixProcessor *downmixerForVis;
VisualizationController *visController;
BOOL enableHrtf;
HeadphoneFilter *hrtf;
BOOL enableFSurround;
BOOL FSurroundDelayRemoved;
int inputBufferLastTime;
FSurroundFilter *fsurround;
BOOL resetStreamFormat;
BOOL shouldPlayOutBuffer;
float *samplePtr;
float tempBuffer[512 * 32];
float rsTempBuffer[4096 * 32];
float inputBuffer[4096 * 32]; // 4096 samples times maximum supported channel count
float fsurroundBuffer[8192 * 6];
float hrtfBuffer[4096 * 2];
float eqBuffer[4096 * 32];
float visAudio[4096];
float visTemp[8192];
#ifdef OUTPUT_LOG
FILE *_logFile;
#endif
}
- (id)initWithController:(OutputNode *)c;
- (BOOL)setup;
- (OSStatus)setOutputDeviceByID:(AudioDeviceID)deviceID;
- (BOOL)setOutputDeviceWithDeviceDict:(NSDictionary *)deviceDict;
- (void)start;
- (void)pause;
- (void)resume;
- (void)stop;
- (double)latency;
- (void)setVolume:(double)v;
- (void)setEqualizerEnabled:(BOOL)enabled;
- (void)setShouldPlayOutBuffer:(BOOL)enabled;
- (void)sustainHDCD;
@end

File diff suppressed because it is too large Load diff

View file

@ -2,8 +2,8 @@
// OutputCoreAudio.h
// Cog
//
// Created by Vincent Spader on 8/2/05.
// Copyright 2005 Vincent Spader. All rights reserved.
// Created by Christopher Snowhill on 7/25/23.
// Copyright 2023-2024 Christopher Snowhill. All rights reserved.
//
#import <AssertMacros.h>
@ -22,41 +22,51 @@ using std::atomic_long;
#import <stdatomic.h>
#endif
#import "Downmix.h"
#import <simd/simd.h>
#import "VisualizationController.h"
#import "Semaphore.h"
#import <CogAudio/ChunkList.h>
#import <CogAudio/HeadphoneFilter.h>
//#define OUTPUT_LOG
#ifdef OUTPUT_LOG
#import <stdio.h>
#endif
@class OutputNode;
@class AudioChunk;
@interface OutputCoreAudio : NSObject {
OutputNode *outputController;
Semaphore *writeSemaphore;
Semaphore *readSemaphore;
dispatch_semaphore_t writeSemaphore;
dispatch_semaphore_t readSemaphore;
NSLock *outputLock;
double streamTimestamp;
BOOL stopInvoked;
BOOL stopCompleted;
BOOL running;
BOOL stopping;
BOOL stopped;
BOOL started;
BOOL paused;
BOOL stopNext;
BOOL restarted;
BOOL commandStop;
BOOL resetting;
BOOL cutOffInput;
BOOL fading, faded;
float fadeLevel;
float fadeStep;
float fadeTarget;
BOOL eqEnabled;
BOOL eqInitialized;
BOOL streamFormatStarted;
BOOL streamFormatChanged;
atomic_long bytesRendered;
atomic_long bytesHdcdSustained;
double secondsHdcdSustained;
BOOL defaultdevicelistenerapplied;
BOOL currentdevicelistenerapplied;
@ -70,46 +80,53 @@ using std::atomic_long;
AVAudioFormat *_deviceFormat;
AudioDeviceID outputDeviceID;
AudioStreamBasicDescription deviceFormat; // info about the default device
AudioStreamBasicDescription deviceFormat;
AudioStreamBasicDescription realStreamFormat; // stream format pre-hrtf
AudioStreamBasicDescription streamFormat; // stream format last seen in render callback
AudioStreamBasicDescription visFormat; // Mono format for vis
uint32_t deviceChannelConfig;
uint32_t realStreamChannelConfig;
uint32_t streamChannelConfig;
AUAudioUnit *_au;
size_t _bufferSize;
AudioUnit _eq;
BOOL resetStreamFormat;
BOOL shouldPlayOutBuffer;
DownmixProcessor *downmixer;
DownmixProcessor *downmixerForVis;
VisualizationController *visController;
os_workgroup_t wg;
os_workgroup_join_token_s wgToken;
ChunkList *outputBuffer;
#ifdef OUTPUT_LOG
FILE *_logFile;
NSFileHandle *_logFile;
#endif
}
- (id)initWithController:(OutputNode *)c;
- (BOOL)setup;
- (OSStatus)setOutputDeviceByID:(AudioDeviceID)deviceID;
- (OSStatus)setOutputDeviceByID:(int)deviceID;
- (BOOL)setOutputDeviceWithDeviceDict:(NSDictionary *)deviceDict;
- (void)start;
- (void)pause;
- (void)resume;
- (void)stop;
- (void)fadeOut;
- (void)fadeOutBackground;
- (void)fadeIn;
- (double)latency;
- (double)volume;
- (void)setVolume:(double)v;
- (void)setEqualizerEnabled:(BOOL)enabled;
- (void)setShouldPlayOutBuffer:(BOOL)enabled;
- (void)sustainHDCD;
- (AudioStreamBasicDescription)deviceFormat;
- (uint32_t)deviceChannelConfig;
@end

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,11 @@
// Plugins! HOORAY!
#if __has_include(<CogAudio/AudioChunk.h>)
# import <CogAudio/AudioChunk.h>
#else
# import "AudioChunk.h"
#endif
@protocol CogSource <NSObject>
+ (NSArray *)schemes; // http, file, etc
@ -25,6 +31,9 @@
+ (float)priority;
+ (NSArray *)urlsForContainerURL:(NSURL *)url;
@optional
+ (NSArray *)dependencyUrlsForContainerURL:(NSURL *)url;
@end
@protocol CogDecoder <NSObject>
@ -39,7 +48,7 @@
- (NSDictionary *)properties;
- (NSDictionary *)metadata; // Only to be implemented for dynamic metadata, send events on change
- (int)readAudio:(void *)buffer frames:(UInt32)frames;
- (AudioChunk *)readAudio;
- (BOOL)open:(id<CogSource>)source;
- (long)seek:(long)frame;
@ -93,6 +102,7 @@
- (id<CogSource>)audioSourceForURL:(NSURL *)url;
- (NSArray *)urlsForContainerURL:(NSURL *)url;
- (NSArray *)dependencyUrlsForContainerURL:(NSURL *)url;
- (NSDictionary *)metadataForURL:(NSURL *)url skipCue:(BOOL)skip;
- (NSDictionary *)propertiesForURL:(NSURL *)url skipCue:(BOOL)skip;
- (id<CogDecoder>)audioDecoderForSource:(id<CogSource>)source skipCue:(BOOL)skip;
@ -100,9 +110,10 @@
- (int)putMetadataInURL:(NSURL *)url;
@end
static NSString *guess_encoding_of_string(const char *input) {
NSString *ret = @"";
NSData *stringData = [NSData dataWithBytes:input length:strlen(input)];
[NSString stringEncodingForData:stringData encodingOptions:nil convertedString:&ret usedLossyConversion:nil];
return ret;
#ifdef __cplusplus
extern "C" {
#endif
extern NSString *guess_encoding_of_string(const char *input);
#ifdef __cplusplus
}
#endif

View file

@ -2,7 +2,7 @@
#import <Cocoa/Cocoa.h>
#import "Plugin.h"
#import <CogAudio/Plugin.h>
// Singletonish
@interface PluginController : NSObject <CogPluginController> {

View file

@ -31,6 +31,7 @@ static std::map<std::string, Cached_Metadata> Cache_List;
static RedundantPlaylistDataStore *Cache_Data_Store = nil;
static bool Cache_Running = false;
static bool Cache_Stopped = false;
static std::thread *Cache_Thread = NULL;
@ -44,6 +45,8 @@ static void cache_init() {
static void cache_deinit() {
Cache_Running = false;
Cache_Thread->join();
while(!Cache_Stopped)
usleep(500);
delete Cache_Thread;
Cache_Data_Store = nil;
}
@ -109,11 +112,16 @@ static NSDictionary *cache_access_metadata(NSURL *url) {
static void cache_run() {
std::chrono::milliseconds dura(250);
Cache_Running = true;
while(Cache_Running) {
std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now();
@autoreleasepool {
std::lock_guard<std::mutex> lock(Cache_Lock);
size_t cacheListOriginalSize = Cache_List.size();
for(auto it = Cache_List.begin(); it != Cache_List.end();) {
auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(now - it->second.time_accessed);
if(elapsed.count() >= 10) {
@ -123,12 +131,15 @@ static void cache_run() {
++it;
}
if(Cache_List.size() == 0)
if(cacheListOriginalSize && Cache_List.size() == 0) {
[Cache_Data_Store reset];
}
}
std::this_thread::sleep_for(dura);
}
Cache_Stopped = true;
}
@implementation PluginController
@ -190,6 +201,9 @@ static PluginController *sharedPluginController = nil;
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(bundleDidLoad:) name:NSBundleDidLoadNotification object:nil];
[self loadPlugins];
[[NSNotificationCenter defaultCenter] removeObserver:self name:NSBundleDidLoadNotification object:nil];
[self printPluginInfo];
}
}
@ -348,6 +362,16 @@ static PluginController *sharedPluginController = nil;
}
}
static NSString *xmlEscapeString(NSString * string) {
CFStringRef textXML = CFXMLCreateStringByEscapingEntities(kCFAllocatorDefault, (CFStringRef)string, nil);
if(textXML) {
NSString *textString = (__bridge NSString *)textXML;
CFRelease(textXML);
return textString;
}
return @"";
}
- (void)printPluginInfo {
ALog(@"Sources: %@", self.sources);
ALog(@"Containers: %@", self.containers);
@ -396,14 +420,16 @@ static PluginController *sharedPluginController = nil;
NSString * plistFooter = @"\t</array>\n\
\t<key>CFBundleExecutable</key>\n\
\t<string>Cog</string>\n\
\t<key>CFBundleHelpBookFolder</key>\n\
\t<string>Cog.help</string>\n\
\t<key>CFBundleHelpBookName</key>\n\
\t<string>org.cogx.cog.help</string>\n\
\t<key>CFBundleIdentifier</key>\n\
\t<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>\n\
\t<key>CFBundleInfoDictionaryVersion</key>\n\
\t<string>6.0</string>\n\
\t<key>CFBundleName</key>\n\
\t<string>$(PRODUCT_NAME)</string>\n\
\t<key>CFBundleDisplayName</key>\n\
\t<string>$(PRODUCT_NAME)</string>\n\
\t<key>CFBundlePackageType</key>\n\
\t<string>APPL</string>\n\
\t<key>CFBundleShortVersionString</key>\n\
@ -441,12 +467,22 @@ static PluginController *sharedPluginController = nil;
\t<string>MediaKeysApplication</string>\n\
\t<key>NSRemindersUsageDescription</key>\n\
\t<string>Cog has no use for your reminders. Why are you trying to access them with an audio player?</string>\n\
\t<key>NSDownloadsFolderUsageDescription</key>\n\
\t<string>We may request related audio files from this folder for playback purposes. We will only play back files you specifically add, unless you enable the option to add an entire folder. Granting permission either for individual files or for parent folders ensures their contents will remain playable in future sessions.</string>\n\
\t<key>NSDocumentsFolderUsageDescription</key>\n\
\t<string>We may request related audio files from this folder for playback purposes. We will only play back files you specifically add, unless you enable the option to add an entire folder. Granting permission either for individual files or for parent folders ensures their contents will remain playable in future sessions.</string>\n\
\t<key>NSDesktopFolderUsageDescription</key>\n\
\t<string>We may request related audio files from this folder for playback purposes. We will only play back files you specifically add, unless you enable the option to add an entire folder. Granting permission either for individual files or for parent folders ensures their contents will remain playable in future sessions.</string>\n\
\t<key>NSMotionUsageDescription</key>\n\
\t<string>Cog optionally supports motion tracking headphones for head tracked positional audio, using its own low latency positioning model.</string>\n\
\t<key>OSAScriptingDefinition</key>\n\
\t<string>Cog.sdef</string>\n\
\t<key>SUFeedURL</key>\n\
\t<string>https://cogcdn.cog.losno.co/mercury.xml</string>\n\
\t<key>SUPublicEDKey</key>\n\
\t<string>omxG7Rp0XK9/YEvKbVy7cd44eVAh1LJB6CmjQwjOJz4=</string>\n\
\t<key>ITSAppUsesNonExemptEncryption</key>\n\
\t<false/>\n\
</dict>\n\
</plist>\n";
NSMutableArray * decodersRegistered = [[NSMutableArray alloc] init];
@ -504,7 +540,7 @@ static PluginController *sharedPluginController = nil;
\t\t\t<integer>1</integer>\n\
\t\t\t<key>CFBundleTypeName</key>\n\
\t\t\t<string>"];
[stringList addObject:[type objectAtIndex:0]];
[stringList addObject:xmlEscapeString([type objectAtIndex:0])];
[stringList addObject:@"</string>\n\
\t\t\t<key>CFBundleTypeRole</key>\n\
\t\t\t<string>Viewer</string>\n\
@ -555,6 +591,29 @@ static PluginController *sharedPluginController = nil;
return [container urlsForContainerURL:url];
}
- (NSArray *)dependencyUrlsForContainerURL:(NSURL *)url {
NSString *ext = [url pathExtension];
NSArray *containerSet = [containers objectForKey:[ext lowercaseString]];
NSString *classString;
if(containerSet) {
if([containerSet count] > 1) {
return [CogContainerMulti dependencyUrlsForContainerURL:url containers:containerSet];
} else {
classString = [containerSet objectAtIndex:0];
}
} else {
return nil;
}
Class container = NSClassFromString(classString);
if([container respondsToSelector:@selector(dependencyUrlsForContainerURL:)]) {
return [container dependencyUrlsForContainerURL:url];
} else {
return nil;
}
}
// Note: Source is assumed to already be opened.
- (id<CogDecoder>)audioDecoderForSource:(id<CogSource>)source skipCue:(BOOL)skip {
NSString *ext = [[source url] pathExtension];
@ -589,11 +648,28 @@ static PluginController *sharedPluginController = nil;
}
}
if(skip && [classString isEqualToString:@"CueSheetDecoder"]) {
classString = @"SilenceDecoder";
}
Class decoder = NSClassFromString(classString);
return [[decoder alloc] init];
}
+ (BOOL)isCoverFile:(NSString *)fileName {
for(NSString *coverFileName in [PluginController coverNames]) {
if([[[[fileName lastPathComponent] stringByDeletingPathExtension] lowercaseString] hasSuffix:coverFileName]) {
return true;
}
}
return false;
}
+ (NSArray *)coverNames {
return @[@"cover", @"folder", @"album", @"front"];
}
- (NSDictionary *)metadataForURL:(NSURL *)url skipCue:(BOOL)skip {
NSString *urlScheme = [url scheme];
if([urlScheme isEqualToString:@"http"] ||
@ -603,36 +679,76 @@ static PluginController *sharedPluginController = nil;
NSDictionary *cacheData = cache_access_metadata(url);
if(cacheData) return cacheData;
NSString *ext = [url pathExtension];
NSArray *readers = [metadataReaders objectForKey:[ext lowercaseString]];
NSString *classString;
if(readers) {
if([readers count] > 1) {
if(skip) {
NSMutableArray *_readers = [readers mutableCopy];
for(int i = 0; i < [_readers count];) {
if([[_readers objectAtIndex:i] isEqualToString:@"CueSheetMetadataReader"])
[_readers removeObjectAtIndex:i];
else
++i;
do {
NSString *ext = [url pathExtension];
NSArray *readers = [metadataReaders objectForKey:[ext lowercaseString]];
NSString *classString;
if(readers) {
if([readers count] > 1) {
if(skip) {
NSMutableArray *_readers = [readers mutableCopy];
for(int i = 0; i < [_readers count];) {
if([[_readers objectAtIndex:i] isEqualToString:@"CueSheetMetadataReader"])
[_readers removeObjectAtIndex:i];
else
++i;
}
cacheData = [CogMetadataReaderMulti metadataForURL:url readers:_readers];
break;
}
cacheData = [CogMetadataReaderMulti metadataForURL:url readers:_readers];
cache_insert_metadata(url, cacheData);
return cacheData;
cacheData = [CogMetadataReaderMulti metadataForURL:url readers:readers];
break;
} else {
classString = [readers objectAtIndex:0];
}
cacheData = [CogMetadataReaderMulti metadataForURL:url readers:readers];
cache_insert_metadata(url, cacheData);
return cacheData;
} else {
classString = [readers objectAtIndex:0];
cacheData = nil;
break;
}
} else {
return nil;
if(skip && [classString isEqualToString:@"CueSheetMetadataReader"]) {
cacheData = nil;
break;
}
Class metadataReader = NSClassFromString(classString);
cacheData = [metadataReader metadataForURL:url];
} while(0);
if(cacheData == nil) {
cacheData = [NSDictionary dictionary];
}
Class metadataReader = NSClassFromString(classString);
if(cacheData) {
NSData *image = [cacheData objectForKey:@"albumArt"];
if(nil == image) {
// Try to load image from external file
NSString *path = [[url path] stringByDeletingLastPathComponent];
// Gather list of candidate image files
NSArray *fileNames = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:path error:nil];
NSArray *types = @[@"jpg", @"jpeg", @"png", @"gif", @"webp", @"avif", @"heic"];
NSArray *imageFileNames = [fileNames pathsMatchingExtensions:types];
for(NSString *fileName in imageFileNames) {
if([PluginController isCoverFile:fileName]) {
image = [NSData dataWithContentsOfFile:[path stringByAppendingPathComponent:fileName]];
break;
}
}
if(image) {
NSMutableDictionary *data = [cacheData mutableCopy];
[data setValue:image forKey:@"albumArt"];
cacheData = data;
}
}
}
cacheData = [metadataReader metadataForURL:url];
cache_insert_metadata(url, cacheData);
return cacheData;
}
@ -714,3 +830,24 @@ static PluginController *sharedPluginController = nil;
}
@end
NSString *guess_encoding_of_string(const char *input) {
NSString *ret = @"";
if(input && *input) {
@try {
ret = [NSString stringWithUTF8String:input];
}
@catch(NSException *e) {
ret = nil;
}
if(!ret) {
// This method is incredibly slow
NSData *stringData = [NSData dataWithBytes:input length:strlen(input)];
[NSString stringEncodingForData:stringData encodingOptions:nil convertedString:&ret usedLossyConversion:nil];
if(!ret) {
ret = @"";
}
}
}
return ret;
}

3141
Audio/ThirdParty/fsurround/channelmaps.cpp vendored Executable file

File diff suppressed because it is too large Load diff

36
Audio/ThirdParty/fsurround/channelmaps.h vendored Executable file
View file

@ -0,0 +1,36 @@
/*
Copyright (C) 2010 Christian Kothe
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#ifndef CHANNELMAPS_H
#define CHANNELMAPS_H
#include "freesurround_decoder.h"
#include <map>
#include <vector>
const int grid_res = 21; // resolution of the lookup grid
// channel allocation maps (per setup)
typedef std::vector<std::vector<float*> > alloc_lut;
extern std::map<unsigned, alloc_lut> chn_alloc;
// channel metadata maps (per setup)
extern std::map<unsigned, std::vector<float> > chn_angle;
extern std::map<unsigned, std::vector<float> > chn_xsf;
extern std::map<unsigned, std::vector<float> > chn_ysf;
extern std::map<unsigned, std::vector<channel_id> > chn_id;
#endif

View file

@ -0,0 +1,413 @@
/*
Copyright (C) 2007-2010 Christian Kothe
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "freesurround_decoder.h"
#include "channelmaps.h"
#include <Accelerate/Accelerate.h>
#include <cmath>
#include <vector>
#pragma warning(disable : 4244)
#define pi _pi
const float _pi = 3.141592654f;
const float epsilon = 0.000001f;
using namespace std;
#undef min
#undef max
static void *_memalign_malloc(size_t size, size_t align) {
void *ret = NULL;
if(posix_memalign(&ret, align, size) != 0) {
return NULL;
}
return ret;
}
static void _dsp_complexalloc(DSPDoubleSplitComplex *cpx, int count) {
cpx->realp = (double *)_memalign_malloc(count * sizeof(double), 16);
cpx->imagp = (double *)_memalign_malloc(count * sizeof(double), 16);
}
static void _dsp_complexfree(DSPDoubleSplitComplex *cpx) {
free(cpx->realp);
free(cpx->imagp);
}
// FreeSurround implementation
class decoder_impl {
public:
// instantiate the decoder with a given channel setup and processing block size (in samples)
decoder_impl(channel_setup setup, unsigned N)
: N(N),
wnd(N), inbuf(3 * N), setup(setup), C((unsigned)chn_alloc[setup].size()),
buffer_empty(true), lt(N), rt(N), dst(N), dstf(N),
dftsetupF(vDSP_DFT_zrop_CreateSetupD(0, N, vDSP_DFT_FORWARD)),
dftsetupB(vDSP_DFT_zrop_CreateSetupD(0, N, vDSP_DFT_INVERSE)) {
_dsp_complexalloc(&lf, N/2 + 1);
_dsp_complexalloc(&rf, N/2 + 1);
// allocate per-channel buffers
outbuf.resize((N + N / 2) * C);
signal.resize(C);
for(unsigned k = 0; k < C; k++)
_dsp_complexalloc(&signal[k], N/2 + 1);
// init the window function
for(unsigned k = 0; k < N; k++)
wnd[k] = sqrt(0.5 * (1 - cos(2 * pi * k / N)) / N);
// set default parameters
set_circular_wrap(90);
set_shift(0);
set_depth(1);
set_focus(0);
set_center_image(1);
set_front_separation(1);
set_rear_separation(1);
set_low_cutoff(40.0 / 22050);
set_high_cutoff(90.0 / 22050);
set_bass_redirection(false);
flush();
}
~decoder_impl() {
_dsp_complexfree(&lf);
_dsp_complexfree(&rf);
for(unsigned k = 0; k < C; k++)
_dsp_complexfree(&signal[k]);
vDSP_DFT_DestroySetupD(dftsetupF);
vDSP_DFT_DestroySetupD(dftsetupB);
}
// decode a stereo chunk, produces a multichannel chunk of the same size (lagged)
float *decode(const float *input) {
// append incoming data to the end of the input buffer
memcpy(&inbuf[N], &input[0], 8 * N);
// process first and second half, overlapped
buffered_decode(&inbuf[0]);
buffered_decode(&inbuf[N]);
// shift last half of the input to the beginning (for overlapping with a future block)
memcpy(&inbuf[0], &inbuf[2 * N], 4 * N);
buffer_empty = false;
return &outbuf[0];
}
// flush the internal buffers
void flush() {
memset(&outbuf[0], 0, outbuf.size() * 4);
memset(&inbuf[0], 0, inbuf.size() * 4);
buffer_empty = true;
}
// number of samples currently held in the buffer
unsigned buffered() {
return buffer_empty ? 0 : N / 2;
}
// set soundfield & rendering parameters
void set_circular_wrap(float v) {
circular_wrap = v;
}
void set_shift(float v) {
shift = v;
}
void set_depth(float v) {
depth = v;
}
void set_focus(float v) {
focus = v;
}
void set_center_image(float v) {
center_image = v;
}
void set_front_separation(float v) {
front_separation = v;
}
void set_rear_separation(float v) {
rear_separation = v;
}
void set_low_cutoff(float v) {
lo_cut = v * (N / 2);
}
void set_high_cutoff(float v) {
hi_cut = v * (N / 2);
}
void set_bass_redirection(bool v) {
use_lfe = v;
}
private:
// helper functions
static inline float sqr(double x) {
return x * x;
}
static inline double amplitude(const DSPDoubleSplitComplex &cpx, size_t index) {
return sqrt(sqr(cpx.realp[index]) + sqr(cpx.imagp[index]));
}
static inline double phase(const DSPDoubleSplitComplex &cpx, size_t index) {
return atan2(cpx.imagp[index], cpx.realp[index]);
}
static inline void polar(double a, double p, DSPDoubleSplitComplex &cpx, size_t index) {
cpx.realp[index] = a * cos(p);
cpx.imagp[index] = a * sin(p);
}
static inline float min(double a, double b) {
return a < b ? a : b;
}
static inline float max(double a, double b) {
return a > b ? a : b;
}
static inline float clamp(double x) {
return max(-1, min(1, x));
}
static inline float sign(double x) {
return x < 0 ? -1 : (x > 0 ? 1 : 0);
}
// get the distance of the soundfield edge, along a given angle
static inline double edgedistance(double a) {
return min(sqrt(1 + sqr(tan(a))), sqrt(1 + sqr(1 / tan(a))));
}
// get the index (and fractional offset!) in a piecewise-linear channel allocation grid
int map_to_grid(double &x) {
double gp = ((x + 1) * 0.5) * (grid_res - 1), i = min(grid_res - 2, floor(gp));
x = gp - i;
return i;
}
// decode a block of data and overlap-add it into outbuf
void buffered_decode(const float *input) {
// demultiplex and apply window function
vDSP_vspdp(input, 2, &lt[0], 1, N);
vDSP_vspdp(input + 1, 2, &rt[0], 1, N);
vDSP_vmulD(&lt[0], 1, &wnd[0], 1, &lt[0], 1, N);
vDSP_vmulD(&rt[0], 1, &wnd[0], 1, &rt[0], 1, N);
// map into spectral domain
vDSP_ctozD((DSPDoubleComplex *)(&lt[0]), 2, &lf, 1, N / 2);
vDSP_ctozD((DSPDoubleComplex *)(&rt[0]), 2, &rf, 1, N / 2);
vDSP_DFT_ExecuteD(dftsetupF, lf.realp, lf.imagp, lf.realp, lf.imagp);
vDSP_DFT_ExecuteD(dftsetupF, rf.realp, rf.imagp, rf.realp, rf.imagp);
for(unsigned c = 0; c < C; c++) {
signal[c].realp[0] = 0;
signal[c].imagp[0] = 0;
signal[c].realp[N/2] = 0;
signal[c].imagp[N/2] = 0;
}
bzero(signal[C - 1].realp, sizeof(double) * (N / 2 + 1));
bzero(signal[C - 1].imagp, sizeof(double) * (N / 2 + 1));
// compute multichannel output signal in the spectral domain
for(unsigned f = 1; f < N / 2; f++) {
// get Lt/Rt amplitudes & phases
double ampL = amplitude(lf, f), ampR = amplitude(rf, f);
double phaseL = phase(lf, f), phaseR = phase(rf, f);
// calculate the amplitude & phase differences
double ampDiff = clamp((ampL + ampR < epsilon) ? 0 : (ampR - ampL) / (ampR + ampL));
double phaseDiff = abs(phaseL - phaseR);
if(phaseDiff > pi) phaseDiff = 2 * pi - phaseDiff;
// decode into x/y soundfield position
double x, y;
transform_decode(ampDiff, phaseDiff, x, y);
// add wrap control
transform_circular_wrap(x, y, circular_wrap);
// add shift control
y = clamp(y - shift);
// add depth control
y = clamp(1 - (1 - y) * depth);
// add focus control
transform_focus(x, y, focus);
// add crossfeed control
x = clamp(x * (front_separation * (1 + y) / 2 + rear_separation * (1 - y) / 2));
// get total signal amplitude
double amp_total = sqrt(ampL * ampL + ampR * ampR);
// and total L/C/R signal phases
double phase_of[] = { phaseL, atan2(lf.imagp[f] + rf.imagp[f], lf.realp[f] + rf.realp[f]), phaseR };
// compute 2d channel map indexes p/q and update x/y to fractional offsets in the map grid
int p = map_to_grid(x), q = map_to_grid(y);
// map position to channel volumes
for(unsigned c = 0; c < C - 1; c++) {
// look up channel map at respective position (with bilinear interpolation) and build the signal
const vector<float *> &a = chn_alloc[setup][c];
polar(amp_total * ((1 - x) * (1 - y) * a[q][p] + x * (1 - y) * a[q][p + 1] + (1 - x) * y * a[q + 1][p] + x * y * a[q + 1][p + 1]),
phase_of[1 + (int)sign(chn_xsf[setup][c])], signal[c], f);
}
// optionally redirect bass
if(use_lfe && f < hi_cut) {
// level of LFE channel according to normalized frequency
double lfe_level = f < lo_cut ? 1 : 0.5 * (1 + cos(pi * (f - lo_cut) / (hi_cut - lo_cut)));
// assign LFE channel
polar(amp_total, phase_of[1], signal[C - 1], f);
signal[C - 1].realp[f] *= lfe_level;
signal[C - 1].imagp[f] *= lfe_level;
// subtract the signal from the other channels
for(unsigned c = 0; c < C - 1; c++) {
signal[c].realp[f] *= (1 - lfe_level);
signal[c].imagp[f] *= (1 - lfe_level);
}
}
}
// shift the last 2/3 to the first 2/3 of the output buffer
memmove(&outbuf[0], &outbuf[C * N / 2], N * C * 4);
// and clear the rest
memset(&outbuf[C * N], 0, C * 4 * N / 2);
// backtransform each channel and overlap-add
for(unsigned c = 0; c < C; c++) {
// back-transform into time domain
vDSP_DFT_ExecuteD(dftsetupB, signal[c].realp, signal[c].imagp, signal[c].realp, signal[c].imagp);
vDSP_ztocD(&signal[c], 1, (DSPDoubleComplex *)(&dst[0]), 2, N / 2);
// add the result to the last 2/3 of the output buffer, windowed (and remultiplex)
vDSP_vmulD(&dst[0], 1, &wnd[0], 1, &dst[0], 1, N);
vDSP_vdpsp(&dst[0], 1, &dstf[0], 1, N);
vDSP_vadd(&outbuf[C * N / 2 + c], C, &dstf[0], 1, &outbuf[C * N / 2 + c], C, N);
}
}
// transform amp/phase difference space into x/y soundfield space
void transform_decode(double a, double p, double &x, double &y) {
x = clamp(1.0047 * a + 0.46804 * a * p * p * p - 0.2042 * a * p * p * p * p + 0.0080586 * a * p * p * p * p * p * p * p - 0.0001526 * a * p * p * p * p * p * p * p * p * p * p - 0.073512 * a * a * a * p - 0.2499 * a * a * a * p * p * p * p + 0.016932 * a * a * a * p * p * p * p * p * p * p - 0.00027707 * a * a * a * p * p * p * p * p * p * p * p * p * p + 0.048105 * a * a * a * a * a * p * p * p * p * p * p * p - 0.0065947 * a * a * a * a * a * p * p * p * p * p * p * p * p * p * p + 0.0016006 * a * a * a * a * a * p * p * p * p * p * p * p * p * p * p * p - 0.0071132 * a * a * a * a * a * a * a * p * p * p * p * p * p * p * p * p + 0.0022336 * a * a * a * a * a * a * a * p * p * p * p * p * p * p * p * p * p * p - 0.0004804 * a * a * a * a * a * a * a * p * p * p * p * p * p * p * p * p * p * p * p);
y = clamp(0.98592 - 0.62237 * p + 0.077875 * p * p - 0.0026929 * p * p * p * p * p + 0.4971 * a * a * p - 0.00032124 * a * a * p * p * p * p * p * p + 9.2491e-006 * a * a * a * a * p * p * p * p * p * p * p * p * p * p + 0.051549 * a * a * a * a * a * a * a * a + 1.0727e-014 * a * a * a * a * a * a * a * a * a * a);
}
// apply a circular_wrap transformation to some position
void transform_circular_wrap(double &x, double &y, double refangle) {
if(refangle == 90)
return;
refangle = refangle * pi / 180;
double baseangle = 90 * pi / 180;
// translate into edge-normalized polar coordinates
double ang = atan2(x, y), len = sqrt(x * x + y * y);
len = len / edgedistance(ang);
// apply circular_wrap transform
if(abs(ang) < baseangle / 2)
// angle falls within the front region (to be enlarged)
ang *= refangle / baseangle;
else
// angle falls within the rear region (to be shrunken)
ang = pi - (-(((refangle - 2 * pi) * (pi - abs(ang)) * sign(ang)) / (2 * pi - baseangle)));
// translate back into soundfield position
len = len * edgedistance(ang);
x = clamp(sin(ang) * len);
y = clamp(cos(ang) * len);
}
// apply a focus transformation to some position
void transform_focus(double &x, double &y, double focus) {
if(focus == 0)
return;
// translate into edge-normalized polar coordinates
double ang = atan2(x, y), len = clamp(sqrt(x * x + y * y) / edgedistance(ang));
// apply focus
len = focus > 0 ? 1 - pow(1 - len, 1 + focus * 20) : pow(len, 1 - focus * 20);
// back-transform into euclidian soundfield position
len = len * edgedistance(ang);
x = clamp(sin(ang) * len);
y = clamp(cos(ang) * len);
}
// constants
unsigned N, C; // number of samples per input/output block, number of output channels
channel_setup setup; // the channel setup
// parameters
float circular_wrap; // angle of the front soundstage around the listener (90<39>=default)
float shift; // forward/backward offset of the soundstage
float depth; // backward extension of the soundstage
float focus; // localization of the sound events
float center_image; // presence of the center speaker
float front_separation; // front stereo separation
float rear_separation; // rear stereo separation
float lo_cut, hi_cut; // LFE cutoff frequencies
bool use_lfe; // whether to use the LFE channel
// FFT data structures
vector<double> lt, rt, dst; // left total, right total (source arrays), time-domain destination buffer array
vector<float> dstf; // float conversion destination array
DSPDoubleSplitComplex lf, rf; // left total / right total in frequency domain
vDSP_DFT_SetupD dftsetupF, dftsetupB; // FFT objects
// buffers
bool buffer_empty; // whether the buffer is currently empty or dirty
vector<float> inbuf; // stereo input buffer (multiplexed)
vector<float> outbuf; // multichannel output buffer (multiplexed)
vector<double> wnd; // the window function, precomputed
vector<DSPDoubleSplitComplex> signal; // the signal to be constructed in every channel, in the frequency domain
};
// implementation of the shell class
freesurround_decoder::freesurround_decoder(channel_setup setup, unsigned blocksize)
: impl(new decoder_impl(setup, blocksize)) {
}
freesurround_decoder::~freesurround_decoder() {
delete impl;
}
float *freesurround_decoder::decode(const float *input) {
return impl->decode(input);
}
void freesurround_decoder::flush() {
impl->flush();
}
void freesurround_decoder::circular_wrap(float v) {
impl->set_circular_wrap(v);
}
void freesurround_decoder::shift(float v) {
impl->set_shift(v);
}
void freesurround_decoder::depth(float v) {
impl->set_depth(v);
}
void freesurround_decoder::focus(float v) {
impl->set_focus(v);
}
void freesurround_decoder::center_image(float v) {
impl->set_center_image(v);
}
void freesurround_decoder::front_separation(float v) {
impl->set_front_separation(v);
}
void freesurround_decoder::rear_separation(float v) {
impl->set_rear_separation(v);
}
void freesurround_decoder::low_cutoff(float v) {
impl->set_low_cutoff(v);
}
void freesurround_decoder::high_cutoff(float v) {
impl->set_high_cutoff(v);
}
void freesurround_decoder::bass_redirection(bool v) {
impl->set_bass_redirection(v);
}
unsigned freesurround_decoder::buffered() {
return impl->buffered();
}
unsigned freesurround_decoder::num_channels(channel_setup s) {
return (unsigned)chn_id[s].size();
}
channel_id freesurround_decoder::channel_at(channel_setup s, unsigned i) {
return i < chn_id[s].size() ? chn_id[s][i] : ci_none;
}

View file

@ -0,0 +1,210 @@
/*
Copyright (C) 2007-2010 Christian Kothe
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#ifndef FREESURROUND_DECODER_H
#define FREESURROUND_DECODER_H
/**
* Identifiers for the supported output channels (from front to back, left to right).
* The ordering here also determines the ordering of interleaved samples in the output signal.
*/
typedef enum channel_id {
ci_none = 0,
ci_front_left = 1 << 1,
ci_front_center_left = 1 << 2,
ci_front_center = 1 << 3,
ci_front_center_right = 1 << 4,
ci_front_right = 1 << 5,
ci_side_front_left = 1 << 6,
ci_side_front_right = 1 << 7,
ci_side_center_left = 1 << 8,
ci_side_center_right = 1 << 9,
ci_side_back_left = 1 << 10,
ci_side_back_right = 1 << 11,
ci_back_left = 1 << 12,
ci_back_center_left = 1 << 13,
ci_back_center = 1 << 14,
ci_back_center_right = 1 << 15,
ci_back_right = 1 << 16,
ci_lfe = 1 << 31
} channel_id;
/**
* The supported output channel setups.
* A channel setup is defined by the set of channels that are present. Here is a graphic
* of the cs_5point1 setup: http://en.wikipedia.org/wiki/File:5_1_channels_(surround_sound)_label.svg
*/
typedef enum channel_setup {
cs_stereo = ci_front_left | ci_front_right | ci_lfe,
cs_3stereo = ci_front_left | ci_front_center | ci_front_right | ci_lfe,
cs_5stereo = ci_front_left | ci_front_center_left | ci_front_center | ci_front_center_right | ci_front_right | ci_lfe,
cs_4point1 = ci_front_left | ci_front_right | ci_back_left | ci_back_right | ci_lfe,
cs_5point1 = ci_front_left | ci_front_center | ci_front_right | ci_back_left | ci_back_right | ci_lfe,
cs_6point1 = ci_front_left | ci_front_center | ci_front_right | ci_side_center_left | ci_side_center_right | ci_back_center | ci_lfe,
cs_7point1 = ci_front_left | ci_front_center | ci_front_right | ci_side_center_left | ci_side_center_right | ci_back_left | ci_back_right | ci_lfe,
cs_7point1_panorama = ci_front_left | ci_front_center_left | ci_front_center | ci_front_center_right | ci_front_right |
ci_side_center_left | ci_side_center_right | ci_lfe,
cs_7point1_tricenter = ci_front_left | ci_front_center_left | ci_front_center | ci_front_center_right | ci_front_right |
ci_back_left | ci_back_right | ci_lfe,
cs_8point1 = ci_front_left | ci_front_center | ci_front_right | ci_side_center_left | ci_side_center_right |
ci_back_left | ci_back_center | ci_back_right | ci_lfe,
cs_9point1_densepanorama = ci_front_left | ci_front_center_left | ci_front_center | ci_front_center_right | ci_front_right |
ci_side_front_left | ci_side_front_right | ci_side_center_left | ci_side_center_right | ci_lfe,
cs_9point1_wrap = ci_front_left | ci_front_center_left | ci_front_center | ci_front_center_right | ci_front_right |
ci_side_center_left | ci_side_center_right | ci_back_left | ci_back_right | ci_lfe,
cs_11point1_densewrap = ci_front_left | ci_front_center_left | ci_front_center | ci_front_center_right | ci_front_right |
ci_side_front_left | ci_side_front_right | ci_side_center_left | ci_side_center_right |
ci_side_back_left | ci_side_back_right | ci_lfe,
cs_13point1_totalwrap = ci_front_left | ci_front_center_left | ci_front_center | ci_front_center_right | ci_front_right |
ci_side_front_left | ci_side_front_right | ci_side_center_left | ci_side_center_right |
ci_side_back_left | ci_side_back_right | ci_back_left | ci_back_right | ci_lfe,
cs_16point1 = ci_front_left | ci_front_center_left | ci_front_center | ci_front_center_right | ci_front_right |
ci_side_front_left | ci_side_front_right | ci_side_center_left | ci_side_center_right | ci_side_back_left |
ci_side_back_right | ci_back_left | ci_back_center_left | ci_back_center | ci_back_center_right | ci_back_right | ci_lfe,
cs_legacy = 0 // same channels as cs_5point1 but different upmixing transform; does not support the focus control
} channel_setup;
/**
* The FreeSurround decoder.
*/
class freesurround_decoder {
public:
/**
* Create an instance of the decoder.
* @param setup The output channel setup -- determines the number of output channels
* and their place in the sound field.
* @param blocksize Granularity at which data is processed by the decode() function.
* Must be a power of two and should correspond to ca. 10ms worth of single-channel
* samples (default is 4096 for 44.1Khz data). Do not make it shorter or longer
* than 5ms to 20ms since the granularity at which locations are decoded
* changes with this.
*/
freesurround_decoder(channel_setup setup = cs_5point1, unsigned blocksize = 4096);
~freesurround_decoder();
/**
* Decode a chunk of stereo sound. The output is delayed by half of the blocksize.
* This function is the only one needed for straightforward decoding.
* @param input Contains exactly blocksize (multiplexed) stereo samples, i.e. 2*blocksize numbers.
* @return A pointer to an internal buffer of exactly blocksize (multiplexed) multichannel samples.
* The actual number of values depends on the number of output channels in the chosen
* channel setup.
*/
float *decode(const float *input);
/**
* Flush the internal buffer.
*/
void flush();
// --- soundfield transformations
// These functions allow to set up geometric transformations of the sound field after it has been decoded.
// The sound field is best pictured as a 2-dimensional square with the listener in its
// center which can be shifted or stretched in various ways before it is sent to the
// speakers. The order in which these transformations are applied is as listed below.
/**
* Allows to wrap the soundfield around the listener in a circular manner.
* Determines the angle of the frontal sound stage relative to the listener, in degrees.
* A setting of 90<EFBFBD> corresponds to standard surround decoding, 180<EFBFBD> stretches the front stage from
* ear to ear, 270<EFBFBD> wraps it around most of the head. The side and rear content of the sound
* field is compressed accordingly behind the listerer. (default: 90, range: [0<EFBFBD>..360<EFBFBD>])
*/
void circular_wrap(float v);
/**
* Allows to shift the soundfield forward or backward.
* Value range: [-1.0..+1.0]. 0 is no offset, positive values move the sound
* forward, negative values move it backwards. (default: 0)
*/
void shift(float v);
/**
* Allows to scale the soundfield backwards.
* Value range: [0.0..+5.0] -- 0 is all compressed to the front, 1 is no change, 5 is scaled 5x backwards (default: 1)
*/
void depth(float v);
/**
* Allows to control the localization (i.e., focality) of sources.
* Value range: [-1.0..+1.0] -- 0 means unchanged, positive means more localized, negative means more ambient (default: 0)
*/
void focus(float v);
// --- rendering parameters
// These parameters control how the sound field is mapped onto speakers.
/**
* Set the presence of the front center channel(s).
* Value range: [0.0..1.0] -- fully present at 1.0, fully replaced by left/right at 0.0 (default: 1).
* The default of 1.0 results in spec-conformant decoding ("movie mode") while a value of 0.7 is
* better suited for music reproduction (which is usually mixed without a center channel).
*/
void center_image(float v);
/**
* Set the front stereo separation.
* Value range: [0.0..inf] -- 1.0 is default, 0.0 is mono.
*/
void front_separation(float v);
/**
* Set the rear stereo separation.
* Value range: [0.0..inf] -- 1.0 is default, 0.0 is mono.
*/
void rear_separation(float v);
// --- bass redirection (to LFE)
/**
* Enable/disable LFE channel (default: false = disabled)
*/
void bass_redirection(bool v);
/**
* Set the lower end of the transition band, in Hz/Nyquist (default: 40/22050).
*/
void low_cutoff(float v);
/**
* Set the upper end of the transition band, in Hz/Nyquist (default: 90/22050).
*/
void high_cutoff(float v);
// --- info
/**
* Number of samples currently held in the buffer.
*/
unsigned buffered();
/**
* Number of channels in the given setup.
*/
static unsigned num_channels(channel_setup s);
/**
* Channel id of the i'th channel in the given setup.
*/
static channel_id channel_at(channel_setup s, unsigned i);
private:
class decoder_impl *impl; // private implementation
};
#endif

View file

@ -1,5 +1,5 @@
/*
Copyright (C) 2010-2022, Christopher Snowhill,
Copyright (C) 2010-2023, Christopher Snowhill,
All rights reserved.
Optimizations by Gumboot
Additional work by Burt P.

25
Audio/ThirdParty/hrtf/Endianness.h vendored Normal file
View file

@ -0,0 +1,25 @@
#pragma once
// The functions provide little endianness to native endianness conversion and back again
#if(defined(_MSC_VER) && defined(_WIN32)) || defined(__APPLE__)
template <typename T>
inline void from_little_endian_inplace(T& x) {
}
template <typename T>
inline T from_little_endian(T x) {
return x;
}
template <typename T>
inline void to_little_endian_inplace(T& x) {
}
template <typename T>
inline T to_little_endian(T x) {
return x;
}
#else
#error "Specify endianness conversion for your platform"
#endif

641
Audio/ThirdParty/hrtf/HrtfData.cpp vendored Normal file
View file

@ -0,0 +1,641 @@
#include "HrtfData.h"
#include "Endianness.h"
#include <algorithm>
#include <cassert>
#include <cmath>
typedef struct {
uint8_t bytes[3];
} sample_int24_t;
const double pi = M_PI;
template <typename T>
void read_stream(std::istream& stream, T& value) {
stream.read(reinterpret_cast<std::istream::char_type*>(&value), sizeof(value));
from_little_endian_inplace(value);
}
HrtfData::HrtfData(std::istream& stream) {
const char required_magic00[] = { 'M', 'i', 'n', 'P', 'H', 'R', '0', '0' };
const char required_magic01[] = { 'M', 'i', 'n', 'P', 'H', 'R', '0', '1' };
const char required_magic02[] = { 'M', 'i', 'n', 'P', 'H', 'R', '0', '2' };
const char required_magic03[] = { 'M', 'i', 'n', 'P', 'H', 'R', '0', '3' };
char actual_magic[sizeof(required_magic03) / sizeof(required_magic03[0])];
stream.read(actual_magic, sizeof(actual_magic));
if(std::equal(std::begin(required_magic03), std::end(required_magic03), std::begin(actual_magic), std::end(actual_magic))) {
LoadHrtf03(stream);
} else if(std::equal(std::begin(required_magic02), std::end(required_magic02), std::begin(actual_magic), std::end(actual_magic))) {
LoadHrtf02(stream);
} else if(std::equal(std::begin(required_magic01), std::end(required_magic01), std::begin(actual_magic), std::end(actual_magic))) {
LoadHrtf01(stream);
} else if(std::equal(std::begin(required_magic00), std::end(required_magic00), std::begin(actual_magic), std::end(actual_magic))) {
LoadHrtf00(stream);
} else {
throw std::logic_error("Bad file format.");
}
}
void HrtfData::LoadHrtf03(std::istream& stream) {
// const uint8_t ChanType_LeftOnly{0};
const uint8_t ChanType_LeftRight{ 1 };
uint32_t sample_rate;
uint8_t channel_type;
uint8_t impulse_response_length;
uint8_t distances_count;
read_stream(stream, sample_rate);
read_stream(stream, channel_type);
read_stream(stream, impulse_response_length);
read_stream(stream, distances_count);
if(!stream || stream.eof()) {
throw std::logic_error("Failed reading file.");
}
if(channel_type > ChanType_LeftRight) {
throw std::logic_error("Invalid channel format.");
}
int channel_count = channel_type == ChanType_LeftRight ? 2 : 1;
std::vector<DistanceData> distances(distances_count);
for(uint8_t i = 0; i < distances_count; i++) {
uint16_t distance;
read_stream(stream, distance);
distances[i].distance = float(distance) / 1000.0f;
uint8_t elevations_count;
read_stream(stream, elevations_count);
distances[i].elevations.resize(elevations_count);
if(!stream || stream.eof()) {
throw std::logic_error("Failed reading file.");
}
for(uint8_t j = 0; j < elevations_count; j++) {
uint8_t azimuth_count;
read_stream(stream, azimuth_count);
distances[i].elevations[j].azimuths.resize(azimuth_count);
}
if(!stream || stream.eof()) {
throw std::logic_error("Failed reading file.");
}
}
const float normalization_factor = 1.0f / 8388608.0f;
for(auto& distance : distances) {
for(auto& elevation : distance.elevations) {
for(auto& azimuth : elevation.azimuths) {
azimuth.impulse_response.resize(impulse_response_length * channel_count);
for(auto& sample : azimuth.impulse_response) {
union {
sample_int24_t sample;
int32_t sample_int;
} sample_union;
sample_union.sample_int = 0;
read_stream(stream, sample_union.sample);
sample_union.sample_int <<= 8;
sample_union.sample_int >>= 8;
sample = sample_union.sample_int * normalization_factor;
}
}
}
}
if(!stream || stream.eof()) {
throw std::logic_error("Failed reading file.");
}
uint8_t longest_delay = 0;
for(auto& distance : distances) {
for(auto& elevation : distance.elevations) {
for(auto& azimuth : elevation.azimuths) {
uint8_t delay;
read_stream(stream, delay);
azimuth.delay = delay;
longest_delay = std::max(longest_delay, delay);
if(channel_type == ChanType_LeftRight) {
read_stream(stream, delay);
azimuth.delay_right = delay;
longest_delay = std::max(longest_delay, delay);
}
}
}
}
if(!stream || stream.eof()) {
throw std::logic_error("Failed reading file.");
}
std::sort(distances.begin(), distances.end(),
[](const DistanceData& lhs, const DistanceData& rhs) noexcept { return lhs.distance > rhs.distance; });
m_distances = std::move(distances);
m_channel_count = channel_count;
m_response_length = impulse_response_length;
m_sample_rate = sample_rate;
m_longest_delay = longest_delay;
}
void HrtfData::LoadHrtf02(std::istream& stream) {
// const uint8_t SampleType_S16{0};
const uint8_t SampleType_S24{ 1 };
// const uint8_t ChanType_LeftOnly{0};
const uint8_t ChanType_LeftRight{ 1 };
uint32_t sample_rate;
uint8_t sample_type;
uint8_t channel_type;
uint8_t impulse_response_length;
uint8_t distances_count;
read_stream(stream, sample_rate);
read_stream(stream, sample_type);
read_stream(stream, channel_type);
read_stream(stream, impulse_response_length);
read_stream(stream, distances_count);
if(!stream || stream.eof()) {
throw std::logic_error("Failed reading file.");
}
if(sample_type > SampleType_S24) {
throw std::logic_error("Invalid sample type.");
}
if(channel_type > ChanType_LeftRight) {
throw std::logic_error("Invalid channel format.");
}
int channel_count = channel_type == ChanType_LeftRight ? 2 : 1;
std::vector<DistanceData> distances(distances_count);
for(uint8_t i = 0; i < distances_count; i++) {
uint16_t distance;
read_stream(stream, distance);
distances[i].distance = float(distance) / 1000.0f;
uint8_t elevations_count;
read_stream(stream, elevations_count);
distances[i].elevations.resize(elevations_count);
if(!stream || stream.eof()) {
throw std::logic_error("Failed reading file.");
}
for(uint8_t j = 0; j < elevations_count; j++) {
uint8_t azimuth_count;
read_stream(stream, azimuth_count);
distances[i].elevations[j].azimuths.resize(azimuth_count);
}
if(!stream || stream.eof()) {
throw std::logic_error("Failed reading file.");
}
}
const float normalization_factor = (sample_type == SampleType_S24) ? 1.0f / 8388608.0f : 1.0f / 32768.0f;
for(auto& distance : distances) {
for(auto& elevation : distance.elevations) {
for(auto& azimuth : elevation.azimuths) {
azimuth.impulse_response.resize(impulse_response_length * channel_count);
if(sample_type == SampleType_S24) {
for(auto& sample : azimuth.impulse_response) {
union {
sample_int24_t sample;
int32_t sample_int;
} sample_union;
sample_union.sample_int = 0;
read_stream(stream, sample_union.sample);
sample_union.sample_int <<= 8;
sample_union.sample_int >>= 8;
sample = sample_union.sample_int * normalization_factor;
}
} else {
for(auto& sample : azimuth.impulse_response) {
int16_t sample_from_file;
read_stream(stream, sample_from_file);
sample = sample_from_file * normalization_factor;
}
}
}
}
}
if(!stream || stream.eof()) {
throw std::logic_error("Failed reading file.");
}
uint8_t longest_delay = 0;
for(auto& distance : distances) {
for(auto& elevation : distance.elevations) {
for(auto& azimuth : elevation.azimuths) {
uint8_t delay;
read_stream(stream, delay);
azimuth.delay = delay;
longest_delay = std::max(longest_delay, delay);
if(channel_type == ChanType_LeftRight) {
read_stream(stream, delay);
azimuth.delay_right = delay;
longest_delay = std::max(longest_delay, delay);
}
}
}
}
if(!stream || stream.eof()) {
throw std::logic_error("Failed reading file.");
}
std::sort(distances.begin(), distances.end(),
[](const DistanceData& lhs, const DistanceData& rhs) noexcept { return lhs.distance > rhs.distance; });
m_distances = std::move(distances);
m_channel_count = channel_count;
m_response_length = impulse_response_length;
m_sample_rate = sample_rate;
m_longest_delay = longest_delay;
}
void HrtfData::LoadHrtf01(std::istream& stream) {
uint32_t sample_rate;
uint8_t impulse_response_length;
read_stream(stream, sample_rate);
read_stream(stream, impulse_response_length);
if(!stream || stream.eof()) {
throw std::logic_error("Failed reading file.");
}
std::vector<DistanceData> distances(1);
distances[0].distance = 1.0;
uint8_t elevations_count;
read_stream(stream, elevations_count);
distances[0].elevations.resize(elevations_count);
if(!stream || stream.eof()) {
throw std::logic_error("Failed reading file.");
}
for(uint8_t i = 0; i < elevations_count; i++) {
uint8_t azimuth_count;
read_stream(stream, azimuth_count);
distances[0].elevations[i].azimuths.resize(azimuth_count);
}
if(!stream || stream.eof()) {
throw std::logic_error("Failed reading file.");
}
const float normalization_factor = 1.0f / 32768.0f;
for(auto& elevation : distances[0].elevations) {
for(auto& azimuth : elevation.azimuths) {
azimuth.impulse_response.resize(impulse_response_length);
for(auto& sample : azimuth.impulse_response) {
int16_t sample_from_file;
read_stream(stream, sample_from_file);
sample = sample_from_file * normalization_factor;
}
}
}
if(!stream || stream.eof()) {
throw std::logic_error("Failed reading file.");
}
uint8_t longest_delay = 0;
for(auto& elevation : distances[0].elevations) {
for(auto& azimuth : elevation.azimuths) {
uint8_t delay;
read_stream(stream, delay);
delay <<= 2;
azimuth.delay = delay;
longest_delay = std::max(longest_delay, delay);
}
}
if(!stream || stream.eof()) {
throw std::logic_error("Failed reading file.");
}
m_distances = std::move(distances);
m_channel_count = 1;
m_response_length = impulse_response_length;
m_sample_rate = sample_rate;
m_longest_delay = longest_delay;
}
void HrtfData::LoadHrtf00(std::istream& stream) {
uint32_t sample_rate;
uint16_t impulse_response_count;
uint16_t impulse_response_length;
read_stream(stream, sample_rate);
read_stream(stream, impulse_response_count);
read_stream(stream, impulse_response_length);
if(!stream || stream.eof()) {
throw std::logic_error("Failed reading file.");
}
std::vector<DistanceData> distances(1);
distances[0].distance = 1.0;
uint8_t elevations_count;
read_stream(stream, elevations_count);
distances[0].elevations.resize(elevations_count);
if(!stream || stream.eof()) {
throw std::logic_error("Failed reading file.");
}
std::vector<uint16_t> irOffsets(elevations_count);
for(uint8_t i = 0; i < elevations_count; i++) {
read_stream(stream, irOffsets[i]);
}
if(!stream || stream.eof()) {
throw std::logic_error("Failed reading file.");
}
for(size_t i = 1; i < elevations_count; i++) {
if(irOffsets[i] <= irOffsets[i - 1]) {
throw std::logic_error("Invalid elevation offset.");
}
}
if(impulse_response_count <= irOffsets[elevations_count - 1]) {
throw std::logic_error("Invalid elevation offset.");
}
for(size_t i = 1; i < elevations_count; i++) {
distances[0].elevations[i - 1].azimuths.resize(irOffsets[i] - irOffsets[i - 1]);
}
distances[0].elevations[elevations_count - 1].azimuths.resize(impulse_response_count - irOffsets[elevations_count - 1]);
const float normalization_factor = 1.0f / 32768.0f;
for(auto& elevation : distances[0].elevations) {
for(auto& azimuth : elevation.azimuths) {
azimuth.impulse_response.resize(impulse_response_length);
for(auto& sample : azimuth.impulse_response) {
int16_t sample_from_file;
read_stream(stream, sample_from_file);
sample = sample_from_file * normalization_factor;
}
}
}
if(!stream || stream.eof()) {
throw std::logic_error("Failed reading file.");
}
uint8_t longest_delay = 0;
for(auto& elevation : distances[0].elevations) {
for(auto& azimuth : elevation.azimuths) {
uint8_t delay;
read_stream(stream, delay);
delay <<= 2;
azimuth.delay = delay;
longest_delay = std::max(longest_delay, delay);
}
}
if(!stream || stream.eof()) {
throw std::logic_error("Failed reading file.");
}
m_distances = std::move(distances);
m_channel_count = 1;
m_response_length = impulse_response_length;
m_sample_rate = sample_rate;
m_longest_delay = longest_delay;
}
void HrtfData::get_direction_data(angle_t elevation, angle_t azimuth, distance_t distance, uint32_t channel, DirectionData& ref_data) const {
assert(elevation >= -angle_t(pi * 0.5));
assert(elevation <= angle_t(pi * 0.5));
assert(azimuth >= -angle_t(2.0 * pi));
assert(azimuth <= angle_t(2.0 * pi));
const float azimuth_mod = std::fmod(azimuth + angle_t(pi * 2.0), angle_t(pi * 2.0));
size_t distance_index0 = 0;
while(distance_index0 < m_distances.size() - 1 &&
m_distances[distance_index0].distance > distance) {
distance_index0++;
}
const size_t distance_index1 = std::min(distance_index0 + 1, m_distances.size() - 1);
const distance_t distance0 = m_distances[distance_index0].distance;
const distance_t distance1 = m_distances[distance_index1].distance;
const distance_t distance_delta = distance0 - distance1;
const float distance_fractional_part = distance_delta ? (distance - distance1) / distance_delta : 0;
const auto& elevations0 = m_distances[distance_index0].elevations;
const auto& elevations1 = m_distances[distance_index1].elevations;
const angle_t elevation_scaled0 = (elevation + angle_t(pi * 0.5)) * (elevations0.size() - 1) / angle_t(pi);
const angle_t elevation_scaled1 = (elevation + angle_t(pi * 0.5)) * (elevations1.size() - 1) / angle_t(pi);
const size_t elevation_index00 = static_cast<size_t>(elevation_scaled0);
const size_t elevation_index10 = static_cast<size_t>(elevation_scaled1);
const size_t elevation_index01 = std::min(elevation_index00 + 1, elevations0.size() - 1);
const size_t elevation_index11 = std::min(elevation_index10 + 1, elevations1.size() - 1);
const float elevation_fractional_part0 = std::fmod(elevation_scaled0, 1.0);
const float elevation_fractional_part1 = std::fmod(elevation_scaled1, 1.0);
const angle_t azimuth_scaled00 = azimuth_mod * elevations0[elevation_index00].azimuths.size() / angle_t(2 * pi);
const size_t azimuth_index000 = static_cast<size_t>(azimuth_scaled00) % elevations0[elevation_index00].azimuths.size();
const size_t azimuth_index001 = static_cast<size_t>(azimuth_scaled00 + 1) % elevations0[elevation_index00].azimuths.size();
const float azimuth_fractional_part00 = std::fmod(azimuth_scaled00, 1.0);
const angle_t azimuth_scaled10 = azimuth_mod * elevations1[elevation_index10].azimuths.size() / angle_t(2 * pi);
const size_t azimuth_index100 = static_cast<size_t>(azimuth_scaled10) % elevations1[elevation_index10].azimuths.size();
const size_t azimuth_index101 = static_cast<size_t>(azimuth_scaled10 + 1) % elevations1[elevation_index10].azimuths.size();
const float azimuth_fractional_part10 = std::fmod(azimuth_scaled10, 1.0);
const angle_t azimuth_scaled01 = azimuth_mod * elevations0[elevation_index01].azimuths.size() / angle_t(2 * pi);
const size_t azimuth_index010 = static_cast<size_t>(azimuth_scaled01) % elevations0[elevation_index01].azimuths.size();
const size_t azimuth_index011 = static_cast<size_t>(azimuth_scaled01 + 1) % elevations0[elevation_index01].azimuths.size();
const float azimuth_fractional_part01 = std::fmod(azimuth_scaled01, 1.0);
const angle_t azimuth_scaled11 = azimuth_mod * elevations1[elevation_index11].azimuths.size() / angle_t(2 * pi);
const size_t azimuth_index110 = static_cast<size_t>(azimuth_scaled11) % elevations1[elevation_index11].azimuths.size();
const size_t azimuth_index111 = static_cast<size_t>(azimuth_scaled11 + 1) % elevations1[elevation_index11].azimuths.size();
const float azimuth_fractional_part11 = std::fmod(azimuth_scaled11, 1.0);
const float blend_factor_000 = (1.0f - elevation_fractional_part0) * (1.0f - azimuth_fractional_part00) * distance_fractional_part;
const float blend_factor_001 = (1.0f - elevation_fractional_part0) * azimuth_fractional_part00 * distance_fractional_part;
const float blend_factor_010 = elevation_fractional_part0 * (1.0f - azimuth_fractional_part01) * distance_fractional_part;
const float blend_factor_011 = elevation_fractional_part0 * azimuth_fractional_part01 * distance_fractional_part;
const float blend_factor_100 = (1.0f - elevation_fractional_part1) * (1.0f - azimuth_fractional_part10) * (1.0f - distance_fractional_part);
const float blend_factor_101 = (1.0f - elevation_fractional_part1) * azimuth_fractional_part10 * (1.0f - distance_fractional_part);
const float blend_factor_110 = elevation_fractional_part1 * (1.0f - azimuth_fractional_part11) * (1.0f - distance_fractional_part);
const float blend_factor_111 = elevation_fractional_part1 * azimuth_fractional_part11 * (1.0f - distance_fractional_part);
delay_t delay0;
delay_t delay1;
if(channel == 0) {
delay0 =
elevations0[elevation_index00].azimuths[azimuth_index000].delay * blend_factor_000 + elevations0[elevation_index00].azimuths[azimuth_index001].delay * blend_factor_001 + elevations0[elevation_index01].azimuths[azimuth_index010].delay * blend_factor_010 + elevations0[elevation_index01].azimuths[azimuth_index011].delay * blend_factor_011;
delay1 =
elevations1[elevation_index10].azimuths[azimuth_index100].delay * blend_factor_100 + elevations1[elevation_index10].azimuths[azimuth_index101].delay * blend_factor_101 + elevations1[elevation_index11].azimuths[azimuth_index110].delay * blend_factor_110 + elevations1[elevation_index11].azimuths[azimuth_index111].delay * blend_factor_111;
} else {
delay0 =
elevations0[elevation_index00].azimuths[azimuth_index000].delay_right * blend_factor_000 + elevations0[elevation_index00].azimuths[azimuth_index001].delay_right * blend_factor_001 + elevations0[elevation_index01].azimuths[azimuth_index010].delay_right * blend_factor_010 + elevations0[elevation_index01].azimuths[azimuth_index011].delay_right * blend_factor_011;
delay1 =
elevations1[elevation_index10].azimuths[azimuth_index100].delay_right * blend_factor_100 + elevations1[elevation_index10].azimuths[azimuth_index101].delay_right * blend_factor_101 + elevations1[elevation_index11].azimuths[azimuth_index110].delay_right * blend_factor_110 + elevations1[elevation_index11].azimuths[azimuth_index111].delay_right * blend_factor_111;
}
ref_data.delay = delay0 + delay1;
if(ref_data.impulse_response.size() < m_response_length)
ref_data.impulse_response.resize(m_response_length);
for(size_t i = 0, j = channel; i < m_response_length; i++, j += m_channel_count) {
float sample0 =
elevations0[elevation_index00].azimuths[azimuth_index000].impulse_response[j] * blend_factor_000 + elevations0[elevation_index00].azimuths[azimuth_index001].impulse_response[j] * blend_factor_001 + elevations0[elevation_index01].azimuths[azimuth_index010].impulse_response[j] * blend_factor_010 + elevations0[elevation_index01].azimuths[azimuth_index011].impulse_response[j] * blend_factor_011;
float sample1 =
elevations1[elevation_index10].azimuths[azimuth_index100].impulse_response[j] * blend_factor_100 + elevations1[elevation_index10].azimuths[azimuth_index101].impulse_response[j] * blend_factor_101 + elevations1[elevation_index11].azimuths[azimuth_index110].impulse_response[j] * blend_factor_110 + elevations1[elevation_index11].azimuths[azimuth_index111].impulse_response[j] * blend_factor_111;
ref_data.impulse_response[i] = sample0 + sample1;
}
}
void HrtfData::get_direction_data(angle_t elevation, angle_t azimuth, distance_t distance, DirectionData& ref_data_left, DirectionData& ref_data_right) const {
assert(elevation >= -angle_t(pi * 0.5));
assert(elevation <= angle_t(pi * 0.5));
assert(azimuth >= -angle_t(2.0 * pi));
assert(azimuth <= angle_t(2.0 * pi));
get_direction_data(elevation, azimuth, distance, 0, ref_data_left);
if(m_channel_count == 1) {
get_direction_data(elevation, -azimuth, distance, 0, ref_data_right);
} else {
get_direction_data(elevation, azimuth, distance, 1, ref_data_right);
}
}
void HrtfData::sample_direction(angle_t elevation, angle_t azimuth, distance_t distance, uint32_t sample, uint32_t channel, float& value, float& delay) const {
assert(elevation >= -angle_t(pi * 0.5));
assert(elevation <= angle_t(pi * 0.5));
assert(azimuth >= -angle_t(2.0 * pi));
assert(azimuth <= angle_t(2.0 * pi));
size_t distance_index0 = 0;
while(distance_index0 < m_distances.size() - 1 &&
m_distances[distance_index0].distance > distance) {
distance_index0++;
}
const size_t distance_index1 = std::min(distance_index0 + 1, m_distances.size() - 1);
const distance_t distance0 = m_distances[distance_index0].distance;
const distance_t distance1 = m_distances[distance_index1].distance;
const distance_t distance_delta = distance0 - distance1;
const float distance_fractional_part = distance_delta ? (distance - distance1) / distance_delta : 0;
const auto& elevations0 = m_distances[distance_index0].elevations;
const auto& elevations1 = m_distances[distance_index1].elevations;
const float azimuth_mod = std::fmod(azimuth + angle_t(pi * 2.0), angle_t(pi * 2.0));
const angle_t elevation_scaled0 = (elevation + angle_t(pi * 0.5)) * (elevations0.size() - 1) / angle_t(pi);
const angle_t elevation_scaled1 = (elevation + angle_t(pi * 0.5)) * (elevations1.size() - 1) / angle_t(pi);
const size_t elevation_index00 = static_cast<size_t>(elevation_scaled0);
const size_t elevation_index10 = static_cast<size_t>(elevation_scaled1);
const size_t elevation_index01 = std::min(elevation_index00 + 1, elevations0.size() - 1);
const size_t elevation_index11 = std::min(elevation_index10 + 1, elevations1.size() - 1);
const float elevation_fractional_part0 = std::fmod(elevation_scaled0, 1.0);
const float elevation_fractional_part1 = std::fmod(elevation_scaled1, 1.0);
const angle_t azimuth_scaled00 = azimuth_mod * elevations0[elevation_index00].azimuths.size() / angle_t(2 * pi);
const size_t azimuth_index000 = static_cast<size_t>(azimuth_scaled00) % elevations0[elevation_index00].azimuths.size();
const size_t azimuth_index001 = static_cast<size_t>(azimuth_scaled00 + 1) % elevations0[elevation_index00].azimuths.size();
const float azimuth_fractional_part00 = std::fmod(azimuth_scaled00, 1.0);
const angle_t azimuth_scaled10 = azimuth_mod * elevations1[elevation_index10].azimuths.size() / angle_t(2 * pi);
const size_t azimuth_index100 = static_cast<size_t>(azimuth_scaled10) % elevations1[elevation_index10].azimuths.size();
const size_t azimuth_index101 = static_cast<size_t>(azimuth_scaled10 + 1) % elevations1[elevation_index10].azimuths.size();
const float azimuth_fractional_part10 = std::fmod(azimuth_scaled10, 1.0);
const angle_t azimuth_scaled01 = azimuth_mod * elevations0[elevation_index01].azimuths.size() / angle_t(2 * pi);
const size_t azimuth_index010 = static_cast<size_t>(azimuth_scaled01) % elevations0[elevation_index01].azimuths.size();
const size_t azimuth_index011 = static_cast<size_t>(azimuth_scaled01 + 1) % elevations0[elevation_index01].azimuths.size();
const float azimuth_fractional_part01 = std::fmod(azimuth_scaled01, 1.0);
const angle_t azimuth_scaled11 = azimuth_mod * elevations1[elevation_index11].azimuths.size() / angle_t(2 * pi);
const size_t azimuth_index110 = static_cast<size_t>(azimuth_scaled11) % elevations1[elevation_index11].azimuths.size();
const size_t azimuth_index111 = static_cast<size_t>(azimuth_scaled11 + 1) % elevations1[elevation_index11].azimuths.size();
const float azimuth_fractional_part11 = std::fmod(azimuth_scaled11, 1.0);
const float blend_factor_000 = (1.0f - elevation_fractional_part0) * (1.0f - azimuth_fractional_part00) * distance_fractional_part;
const float blend_factor_001 = (1.0f - elevation_fractional_part0) * azimuth_fractional_part00 * distance_fractional_part;
const float blend_factor_010 = elevation_fractional_part0 * (1.0f - azimuth_fractional_part01) * distance_fractional_part;
const float blend_factor_011 = elevation_fractional_part0 * azimuth_fractional_part01 * distance_fractional_part;
const float blend_factor_100 = (1.0f - elevation_fractional_part1) * (1.0f - azimuth_fractional_part10) * (1.0f - distance_fractional_part);
const float blend_factor_101 = (1.0f - elevation_fractional_part1) * azimuth_fractional_part10 * (1.0f - distance_fractional_part);
const float blend_factor_110 = elevation_fractional_part1 * (1.0f - azimuth_fractional_part11) * (1.0f - distance_fractional_part);
const float blend_factor_111 = elevation_fractional_part1 * azimuth_fractional_part11 * (1.0f - distance_fractional_part);
float delay0;
float delay1;
if(channel == 0) {
delay0 =
elevations0[elevation_index00].azimuths[azimuth_index000].delay * blend_factor_000 + elevations0[elevation_index00].azimuths[azimuth_index001].delay * blend_factor_001 + elevations0[elevation_index01].azimuths[azimuth_index010].delay * blend_factor_010 + elevations0[elevation_index01].azimuths[azimuth_index011].delay * blend_factor_011;
delay1 =
elevations1[elevation_index10].azimuths[azimuth_index100].delay * blend_factor_100 + elevations1[elevation_index10].azimuths[azimuth_index101].delay * blend_factor_101 + elevations1[elevation_index11].azimuths[azimuth_index110].delay * blend_factor_110 + elevations1[elevation_index11].azimuths[azimuth_index111].delay * blend_factor_111;
} else {
delay0 =
elevations0[elevation_index00].azimuths[azimuth_index000].delay_right * blend_factor_000 + elevations0[elevation_index00].azimuths[azimuth_index001].delay_right * blend_factor_001 + elevations0[elevation_index01].azimuths[azimuth_index010].delay_right * blend_factor_010 + elevations0[elevation_index01].azimuths[azimuth_index011].delay_right * blend_factor_011;
delay1 =
elevations1[elevation_index10].azimuths[azimuth_index100].delay_right * blend_factor_100 + elevations1[elevation_index10].azimuths[azimuth_index101].delay_right * blend_factor_101 + elevations1[elevation_index11].azimuths[azimuth_index110].delay_right * blend_factor_110 + elevations1[elevation_index11].azimuths[azimuth_index111].delay_right * blend_factor_111;
}
delay = delay0 + delay1;
sample = sample * m_channel_count + channel;
float value0 =
elevations0[elevation_index00].azimuths[azimuth_index000].impulse_response[sample] * blend_factor_000 + elevations0[elevation_index00].azimuths[azimuth_index001].impulse_response[sample] * blend_factor_001 + elevations0[elevation_index01].azimuths[azimuth_index010].impulse_response[sample] * blend_factor_010 + elevations0[elevation_index01].azimuths[azimuth_index011].impulse_response[sample] * blend_factor_011;
float value1 =
elevations1[elevation_index10].azimuths[azimuth_index100].impulse_response[sample] * blend_factor_100 + elevations1[elevation_index10].azimuths[azimuth_index101].impulse_response[sample] * blend_factor_101 + elevations1[elevation_index11].azimuths[azimuth_index110].impulse_response[sample] * blend_factor_110 + elevations1[elevation_index11].azimuths[azimuth_index111].impulse_response[sample] * blend_factor_111;
value = value0 + value1;
}
void HrtfData::sample_direction(angle_t elevation, angle_t azimuth, distance_t distance, uint32_t sample, float& value_left, float& delay_left, float& value_right, float& delay_right) const {
assert(elevation >= -angle_t(pi * 0.5));
assert(elevation <= angle_t(pi * 0.5));
assert(azimuth >= -angle_t(2.0 * pi));
assert(azimuth <= angle_t(2.0 * pi));
sample_direction(elevation, azimuth, distance, sample, 0, value_left, delay_left);
if(m_channel_count == 1) {
sample_direction(elevation, -azimuth, distance, sample, 0, value_right, delay_right);
} else {
sample_direction(elevation, azimuth, distance, sample, 1, value_right, delay_right);
}
}

48
Audio/ThirdParty/hrtf/HrtfData.h vendored Normal file
View file

@ -0,0 +1,48 @@
#pragma once
#include "HrtfTypes.h"
#include "IHrtfData.h"
#include <cstdint>
#include <iostream>
#include <vector>
struct ElevationData {
std::vector<DirectionData> azimuths;
};
struct DistanceData {
distance_t distance;
std::vector<ElevationData> elevations;
};
class HrtfData : public IHrtfData {
void LoadHrtf00(std::istream& stream);
void LoadHrtf01(std::istream& stream);
void LoadHrtf02(std::istream& stream);
void LoadHrtf03(std::istream& stream);
public:
HrtfData(std::istream& stream);
void get_direction_data(angle_t elevation, angle_t azimuth, distance_t distance, uint32_t channel, DirectionData& ref_data) const override;
void get_direction_data(angle_t elevation, angle_t azimuth, distance_t distance, DirectionData& ref_data_left, DirectionData& ref_data_right) const override;
void sample_direction(angle_t elevation, angle_t azimuth, distance_t distance, uint32_t sample, uint32_t channel, float& value, float& delay) const override;
void sample_direction(angle_t elevation, angle_t azimuth, distance_t distance, uint32_t sample, float& value_left, float& delay_left, float& value_right, float& delay_right) const override;
uint32_t get_sample_rate() const override {
return m_sample_rate;
}
uint32_t get_response_length() const override {
return m_response_length;
}
uint32_t get_longest_delay() const override {
return m_longest_delay;
}
private:
uint32_t m_sample_rate;
uint32_t m_response_length;
uint32_t m_longest_delay;
uint32_t m_channel_count;
std::vector<DistanceData> m_distances;
};

14
Audio/ThirdParty/hrtf/HrtfTypes.h vendored Normal file
View file

@ -0,0 +1,14 @@
#pragma once
#include <cstdint>
#include <vector>
typedef float distance_t;
typedef float angle_t;
typedef int delay_t;
struct DirectionData {
std::vector<float> impulse_response;
delay_t delay;
delay_t delay_right;
};

19
Audio/ThirdParty/hrtf/IHrtfData.h vendored Normal file
View file

@ -0,0 +1,19 @@
#pragma once
#include "HrtfTypes.h"
class IHrtfData {
public:
virtual ~IHrtfData() = default;
virtual void get_direction_data(angle_t elevation, angle_t azimuth, distance_t distance, uint32_t channel, DirectionData& ref_data) const = 0;
virtual void get_direction_data(angle_t elevation, angle_t azimuth, distance_t distance, DirectionData& ref_data_left, DirectionData& ref_data_right) const = 0;
// Get only once IR sample at given direction. The delay returned is the delay of IR's beginning, not the sample's!
virtual void sample_direction(angle_t elevation, angle_t azimuth, distance_t distance, uint32_t sample, uint32_t channel, float& value, float& delay) const = 0;
// Get only once IR sample at given direction for both channels. The delay returned is the delay of IR's beginning, not the sample's!
virtual void sample_direction(angle_t elevation, angle_t azimuth, distance_t distance, uint32_t sample, float& value_left, float& delay_left, float& value_right, float& delay_right) const = 0;
virtual uint32_t get_sample_rate() const = 0;
virtual uint32_t get_response_length() const = 0;
virtual uint32_t get_longest_delay() const = 0;
};

View file

@ -139,7 +139,10 @@ static void vorbis_lpc_predict(float *coeff, float *prime, int m, float *data, l
}
void lpc_extrapolate2(float *const data, const size_t data_len, const int nch, const int lpc_order, const size_t extra_bkwd, const size_t extra_fwd, void **extrapolate_buffer, size_t *extrapolate_buffer_size) {
const size_t tdata_size = sizeof(float) * (extra_bkwd + data_len + extra_fwd);
const size_t max_to_prime = (data_len < lpc_order) ? data_len : lpc_order;
const size_t min_data_len = (data_len < lpc_order) ? lpc_order : data_len;
const size_t tdata_size = sizeof(float) * (extra_bkwd + min_data_len + extra_fwd);
const size_t aut_size = sizeof(double) * (lpc_order + 1);
const size_t lpc_size = sizeof(double) * lpc_order;
const size_t lpci_size = sizeof(float) * lpc_order;
@ -152,42 +155,65 @@ void lpc_extrapolate2(float *const data, const size_t data_len, const int nch, c
*extrapolate_buffer_size = new_size;
}
float *tdata = (float *)(*extrapolate_buffer); // for 1 channel only
double *aut = (double *)(*extrapolate_buffer);
double *lpc = (double *)(*extrapolate_buffer + aut_size);
double *aut = (double *)(*extrapolate_buffer + tdata_size);
double *lpc = (double *)(*extrapolate_buffer + tdata_size + aut_size);
float *lpci = (float *)(*extrapolate_buffer + tdata_size + aut_size + lpc_size);
float *work = (float *)(*extrapolate_buffer + tdata_size + aut_size + lpc_size + lpci_size);
float *tdata = (float *)(*extrapolate_buffer + aut_size + lpc_size); // for 1 channel only
float *lpci = (float *)(*extrapolate_buffer + aut_size + lpc_size + tdata_size);
float *work = (float *)(*extrapolate_buffer + aut_size + lpc_size + tdata_size + lpci_size);
for(int c = 0; c < nch; c++) {
if(extra_bkwd) {
for(int i = 0; i < (int)data_len; i++)
tdata[data_len - 1 - i] = data[i * nch + c];
tdata[min_data_len - 1 - i] = data[i * nch + c];
if(data_len < min_data_len)
for(int i = (int)data_len; i < (int)min_data_len; i++)
tdata[min_data_len - 1 - i] = 0.0f;
} else {
for(int i = 0; i < (int)data_len; i++)
tdata[i] = data[i * nch + c];
const ssize_t len_diff = min_data_len - data_len;
if(len_diff <= 0) {
for(int i = 0; i < (int)data_len; i++)
tdata[i] = data[i * nch + c];
} else {
for(int i = 0; i < (int)len_diff; i++)
tdata[i] = 0.0f;
for(int i = 0; i < (int)data_len; i++)
tdata[len_diff + i] = data[i * nch + c];
}
}
apply_window(tdata, data_len);
vorbis_lpc_from_data(tdata, lpci, (int)data_len, lpc_order, aut, lpc);
apply_window(tdata, min_data_len);
vorbis_lpc_from_data(tdata, lpci, (int)min_data_len, lpc_order, aut, lpc);
// restore after apply_window
if(extra_bkwd) {
for(int i = 0; i < (int)data_len; i++)
tdata[data_len - 1 - i] = data[i * nch + c];
tdata[min_data_len - 1 - i] = data[i * nch + c];
if(data_len < min_data_len)
for(int i = (int)data_len; i < (int)min_data_len; i++)
tdata[min_data_len - 1 - i] = 0.0f;
} else {
for(int i = 0; i < (int)data_len; i++)
tdata[i] = data[i * nch + c];
const ssize_t len_diff = min_data_len - data_len;
if(len_diff <= 0) {
for(int i = 0; i < (int)data_len; i++)
tdata[i] = data[i * nch + c];
} else {
for(int i = 0; i < (int)len_diff; i++)
tdata[i] = 0.0f;
for(int i = 0; i < (int)data_len; i++)
tdata[len_diff + i] = data[i * nch + c];
}
}
vorbis_lpc_predict(lpci, tdata + data_len - lpc_order, lpc_order, tdata + data_len, extra_fwd + extra_bkwd, work);
vorbis_lpc_predict(lpci, tdata + min_data_len - lpc_order, lpc_order, tdata + min_data_len, extra_fwd + extra_bkwd, work);
if(extra_bkwd) {
for(int i = 0; i < extra_bkwd; i++)
data[(-i - 1) * nch + c] = tdata[data_len + i];
data[(-i - 1) * nch + c] = tdata[min_data_len + i];
} else {
for(int i = 0; i < extra_fwd; i++)
data[(i + data_len) * nch + c] = tdata[data_len + i];
data[(i + data_len) * nch + c] = tdata[min_data_len + i];
}
}
}

View file

@ -24,16 +24,8 @@
static const size_t LPC_ORDER = 32;
#ifdef __cplusplus
extern "C" {
#endif
void lpc_extrapolate2(float * const data, const size_t data_len, const int nch, const int lpc_order, const size_t extra_bkwd, const size_t extra_fwd, void ** extrapolate_buffer, size_t * extrapolate_buffer_size);
#ifdef __cplusplus
}
#endif
static inline void lpc_extrapolate_bkwd(float * const data, const size_t data_len, const size_t prime_len, const int nch, const int lpc_order, const size_t extra_bkwd, void ** extrapolate_buffer, size_t * extrapolate_buffer_size)
{
(void)data_len;

View file

@ -42,7 +42,7 @@ static inline unsigned local_gcd(unsigned a, unsigned b)
*/
static void samples_len(unsigned* r1, unsigned* r2, unsigned N, unsigned M) // example: r1 = 44100, r2 = 48000, N = 20, M = 8192
{
if (r1 == 0 || r2 == 0 || *r1 == 0 || *r2 == 0) return;
if (r1 == 0 || r2 == 0) return;
unsigned v = local_gcd(*r1, *r2); // v = 300
*r1 /= v; *r2 /= v; // r1 = 147; r2 = 160 == 1/300th of second
unsigned n = (v + N-1) / N; // n = 300/20 = 15 times

@ -1 +0,0 @@
Subproject commit afd61e7ed76d86a9bc6cb91fd0a9f305f853fe38

View file

@ -1,164 +0,0 @@
//
// r8bstate.h
// CogAudio Framework
//
// Created by Christopher Snowhill on 3/3/22.
//
#ifndef r8bstate_h
#define r8bstate_h
#include <Accelerate/Accelerate.h>
#include "r8bbase.h"
#include "CDSPResampler.h"
struct r8bstate {
int channelCount;
int bufferCapacity;
size_t remainder;
uint64_t inProcessed;
uint64_t outProcessed;
double sampleRatio;
r8b::CFixedBuffer<double> InBuf;
r8b::CFixedBuffer<double> *OutBufs;
r8b::CDSPResampler24 **Resamps;
r8bstate(int _channelCount, int _bufferCapacity, double srcRate, double dstRate)
: channelCount(_channelCount), bufferCapacity(_bufferCapacity), inProcessed(0), outProcessed(0), remainder(0) {
InBuf.alloc(bufferCapacity);
OutBufs = new r8b::CFixedBuffer<double>[channelCount];
Resamps = new r8b::CDSPResampler24 *[channelCount];
for(int i = 0; i < channelCount; ++i) {
Resamps[i] = new r8b::CDSPResampler24(srcRate, dstRate, bufferCapacity);
}
sampleRatio = dstRate / srcRate;
}
~r8bstate() {
delete[] OutBufs;
for(int i = 0; i < channelCount; ++i) {
delete Resamps[i];
}
delete[] Resamps;
}
double latency() {
return ((double)inProcessed * sampleRatio) - (double)outProcessed;
}
int resample(const float *input, size_t inCount, size_t *inDone, float *output, size_t outMax) {
int ret = 0;
int i;
if(inDone) *inDone = 0;
while(remainder > 0) {
size_t blockCount = remainder;
if(blockCount > outMax)
blockCount = outMax;
for(i = 0; i < channelCount; ++i) {
vDSP_vdpsp(&OutBufs[i][0], 1, output + i, channelCount, blockCount);
}
remainder -= blockCount;
if(remainder > 0) {
for(i = 0; i < channelCount; ++i) {
memmove(&OutBufs[i][0], &OutBufs[i][blockCount], remainder * sizeof(double));
}
}
output += channelCount * blockCount;
outProcessed += blockCount;
outMax -= blockCount;
ret += blockCount;
if(!outMax)
return ret;
}
while(inCount > 0) {
size_t blockCount = inCount;
if(blockCount > bufferCapacity)
blockCount = bufferCapacity;
int outputDone = 0;
for(i = 0; i < channelCount; ++i) {
double *outputPointer;
vDSP_vspdp(input + i, channelCount, &InBuf[0], 1, blockCount);
outputDone = Resamps[i]->process(InBuf, (int)blockCount, outputPointer);
if(outputDone) {
if(outputDone > outMax) {
vDSP_vdpsp(outputPointer, 1, output + i, channelCount, outMax);
remainder = outputDone - outMax;
OutBufs[i].alloc((int)remainder);
memcpy(&OutBufs[i][0], outputPointer + outMax, remainder);
} else {
vDSP_vdpsp(outputPointer, 1, output + i, channelCount, outputDone);
}
}
}
size_t outputActual = outputDone - remainder;
input += channelCount * blockCount;
output += channelCount * outputActual;
inCount -= blockCount;
if(inDone) *inDone += blockCount;
inProcessed += blockCount;
outProcessed += outputActual;
outMax -= outputActual;
ret += outputActual;
if(remainder)
break;
}
return ret;
}
int flush(float *output, size_t outMax) {
int ret = 0;
int i;
if(remainder > 0) {
size_t blockCount = remainder;
if(blockCount > outMax)
blockCount = outMax;
for(i = 0; i < channelCount; ++i) {
vDSP_vdpsp(&OutBufs[i][0], 1, output + i, channelCount, blockCount);
}
remainder -= blockCount;
if(remainder > 0) {
for(i = 0; i < channelCount; ++i) {
memmove(&OutBufs[i][0], &OutBufs[i][blockCount], remainder * sizeof(double));
}
}
output += channelCount * blockCount;
outProcessed += blockCount;
outMax -= blockCount;
ret += blockCount;
if(!outMax)
return ret;
}
uint64_t outputWanted = ceil(inProcessed * sampleRatio);
memset(&InBuf[0], 0, sizeof(double) * bufferCapacity);
while(outProcessed < outputWanted) {
int outputDone = 0;
for(int i = 0; i < channelCount; ++i) {
double *outputPointer;
outputDone = Resamps[i]->process(InBuf, bufferCapacity, outputPointer);
if(outputDone) {
if(outputDone > (outputWanted - outProcessed))
outputDone = (int)(outputWanted - outProcessed);
if(outputDone > outMax) {
vDSP_vdpsp(outputPointer, 1, output + i, channelCount, outMax);
remainder = outputDone - outMax;
OutBufs[i].alloc((int)remainder);
memcpy(&OutBufs[i][0], outputPointer + outMax, remainder);
} else {
vDSP_vdpsp(outputPointer, 1, output + i, channelCount, outputDone);
}
}
}
size_t outputActual = outputDone - remainder;
outProcessed += outputActual;
output += channelCount * outputActual;
outMax -= outputActual;
ret += outputActual;
if(remainder)
break;
}
return ret;
}
};
#endif /* r8bstate_h */

30
Audio/ThirdParty/rsstate.cpp vendored Normal file
View file

@ -0,0 +1,30 @@
//
// rsstate.cpp
// CogAudio Framework
//
// Created by Christopher Snowhill on 2/4/23.
//
#include "rsstate.h"
#include "rsstate.hpp"
void *rsstate_new(int channelCount, double srcRate, double dstRate) {
return (void *)new rsstate(channelCount, srcRate, dstRate);
}
void rsstate_delete(void *state) {
delete(rsstate *)state;
}
double rsstate_latency(void *state) {
return ((rsstate *)state)->latency();
}
int rsstate_resample(void *state, const float *input, size_t inCount, size_t *inDone,
float *output, size_t outMax) {
return ((rsstate *)state)->resample(input, inCount, inDone, output, outMax);
}
int rsstate_flush(void *state, float *output, size_t outMax) {
return ((rsstate *)state)->flush(output, outMax);
}

32
Audio/ThirdParty/rsstate.h vendored Normal file
View file

@ -0,0 +1,32 @@
//
// rsstate.h
// CogAudio Framework
//
// Created by Christopher Snowhill on 2/4/23.
//
#include <stdint.h>
#include <stdlib.h>
#ifndef rsstate_h
#define rsstate_h
#ifdef __cplusplus
extern "C" {
#endif
void *rsstate_new(int channelCount, double srcRate, double dstRate);
void rsstate_delete(void *);
double rsstate_latency(void *);
int rsstate_resample(void *, const float *input, size_t inCount, size_t *inDone,
float *output, size_t outMax);
int rsstate_flush(void *, float *output, size_t outMax);
#ifdef __cplusplus
}
#endif
#endif /* rsstate_h */

81
Audio/ThirdParty/rsstate.hpp vendored Normal file
View file

@ -0,0 +1,81 @@
//
// rsstate.hpp
// CogAudio Framework
//
// Created by Christopher Snowhill on 2/3/23.
//
#ifndef rsstate_hpp
#define rsstate_hpp
#include "soxr.h"
#include <cmath>
#include <vector>
struct rsstate {
int channelCount;
int bufferCapacity;
size_t remainder;
uint64_t inProcessed;
uint64_t outProcessed;
double sampleRatio;
double dstRate;
std::vector<float> SilenceBuf;
soxr_t Resampler;
rsstate(int _channelCount, double srcRate, double _dstRate)
: channelCount(_channelCount), inProcessed(0), outProcessed(0), remainder(0), dstRate(_dstRate) {
SilenceBuf.resize(1024 * channelCount);
memset(&SilenceBuf[0], 0, 1024 * channelCount * sizeof(float));
Resampler = soxr_create(srcRate, dstRate, channelCount, NULL, NULL, NULL, NULL);
sampleRatio = dstRate / srcRate;
}
~rsstate() {
soxr_delete(Resampler);
}
double latency() {
return (((double)inProcessed * sampleRatio) - (double)outProcessed) / dstRate;
}
int resample(const float *input, size_t inCount, size_t *inDone, float *output, size_t outMax) {
size_t outDone = 0;
soxr_error_t errmsg = soxr_process(Resampler, (soxr_in_t)input, inCount, inDone, (soxr_out_t)output, outMax, &outDone);
if(!errmsg) {
inProcessed += *inDone;
outProcessed += outDone;
return (int)outDone;
} else {
return 0;
}
}
int flush(float *output, size_t outMax) {
size_t outTotal = 0;
uint64_t outputWanted = std::ceil(inProcessed * sampleRatio);
while(outProcessed < outputWanted) {
size_t outWanted = outputWanted - outProcessed;
if(outWanted > outMax) {
outWanted = outMax;
}
size_t outDone = 0;
size_t inDone = 0;
soxr_error_t errmsg = soxr_process(Resampler, (soxr_in_t)(&SilenceBuf[0]), 1024, &inDone, (soxr_out_t)output, outWanted, &outDone);
if(!errmsg) {
outProcessed += outDone;
outTotal += outDone;
output += outDone * channelCount;
outMax -= outDone;
if(!outMax || outProcessed == outputWanted) {
return (int)outTotal;
}
} else {
return 0;
}
}
return (int)outTotal;
}
};
#endif /* r8bstate_h */

View file

@ -1,5 +1,5 @@
//
// Semaphore.h
// CogSemaphore.h
// Cog
//
// Created by Vincent Spader on 8/2/05.

View file

@ -1,12 +1,12 @@
//
// Semaphore.m
// CogSemaphore.m
// Cog
//
// Created by Vincent Spader on 8/2/05.
// Copyright 2005 Vincent Spader. All rights reserved.
//
#import "Semaphore.h"
#import <CogAudio/CogSemaphore.h>
@implementation Semaphore

View file

@ -10,16 +10,20 @@
NS_ASSUME_NONNULL_BEGIN
@interface VisualizationController : NSObject {
double sampleRate;
float visAudio[4096];
}
+ (VisualizationController *)sharedController;
- (void)postLatency:(double)latency;
- (UInt64)samplesPosted;
- (void)postSampleRate:(double)sampleRate;
- (void)postVisPCM:(const float *)inPCM amount:(int)amount;
- (double)readSampleRate;
- (void)copyVisPCM:(float *)outPCM visFFT:(float *)outFFT;
- (void)copyVisPCM:(float *_Nullable)outPCM visFFT:(float *_Nullable)outFFT latencyOffset:(double)latency;
- (void)reset;
@end

View file

@ -10,7 +10,13 @@
#import "fft.h"
@implementation VisualizationController
@implementation VisualizationController {
double sampleRate;
double latency;
float *visAudio;
int visAudioCursor, visAudioSize;
uint64_t visSamplesPosted;
}
static VisualizationController *_sharedController = nil;
@ -26,7 +32,9 @@ static VisualizationController *_sharedController = nil;
- (id)init {
self = [super init];
if(self) {
vDSP_vclr(visAudio, 1, 4096);
visAudio = NULL;
visAudioSize = 0;
latency = 0;
}
return self;
}
@ -35,35 +43,129 @@ static VisualizationController *_sharedController = nil;
fft_free();
}
- (void)reset {
@synchronized (self) {
latency = 0;
visAudioCursor = 0;
visSamplesPosted = 0;
if(visAudio && visAudioSize) {
bzero(visAudio, sizeof(float) * visAudioSize);
}
}
}
- (void)postSampleRate:(double)sampleRate {
@synchronized(self) {
self->sampleRate = sampleRate;
if(self->sampleRate != sampleRate) {
self->sampleRate = sampleRate;
int visAudioSize = (int)(sampleRate * 45.0);
void *visAudio = realloc(self->visAudio, visAudioSize * sizeof(float));
if(visAudio && visAudioSize) {
if(visAudioSize > self->visAudioSize) {
bzero(((float *)visAudio) + self->visAudioSize, sizeof(float) * (visAudioSize - self->visAudioSize));
}
self->visAudio = visAudio;
self->visAudioSize = visAudioSize;
visAudioCursor %= visAudioSize;
} else {
if(self->visAudio) {
free(self->visAudio);
self->visAudio = NULL;
}
self->visAudioSize = 0;
}
}
}
}
- (void)postVisPCM:(const float *)inPCM amount:(int)amount {
int skipAmount = 0;
if(amount > 4096) {
skipAmount = amount - 4096;
amount = 4096;
}
@synchronized(self) {
cblas_scopy(4096 - amount, visAudio + amount, 1, visAudio, 1);
cblas_scopy(amount, inPCM + skipAmount, 1, visAudio + 4096 - amount, 1);
if(!visAudioSize) {
return;
}
int samplesRead = 0;
while(amount > 0) {
int amountToCopy = (int)(visAudioSize - visAudioCursor);
if(amountToCopy > amount) amountToCopy = amount;
cblas_scopy(amountToCopy, inPCM + samplesRead, 1, visAudio + visAudioCursor, 1);
visAudioCursor = visAudioCursor + amountToCopy;
if(visAudioCursor >= visAudioSize) visAudioCursor -= visAudioSize;
amount -= amountToCopy;
samplesRead += amountToCopy;
visSamplesPosted += amountToCopy;
}
}
}
- (void)postLatency:(double)latency {
self->latency = latency;
assert(latency < 45.0);
}
- (double)readSampleRate {
@synchronized(self) {
return sampleRate;
}
}
- (void)copyVisPCM:(float *)outPCM visFFT:(float *)outFFT {
@synchronized(self) {
cblas_scopy(4096, visAudio, 1, outPCM, 1);
fft_calculate(visAudio, outFFT, 2048);
- (UInt64)samplesPosted {
return visSamplesPosted;
}
- (void)copyVisPCM:(float *_Nullable)outPCM visFFT:(float *_Nullable)outFFT latencyOffset:(double)latency {
if(!outPCM && !outFFT) return;
if(!visAudio || !visAudioSize) {
if(outPCM) bzero(outPCM, sizeof(float) * 4096);
if(outFFT) bzero(outFFT, sizeof(float) * 2048);
return;
}
void *visAudioTemp = calloc(sizeof(float), 4096);
if(!visAudioTemp) {
if(outPCM) bzero(outPCM, sizeof(float) * 4096);
if(outFFT) bzero(outFFT, sizeof(float) * 2048);
return;
}
@synchronized(self) {
if(!sampleRate) {
bzero(outPCM, 4096 * sizeof(float));
if(outFFT) {
bzero(outFFT, 2048 * sizeof(float));
}
return;
}
int latencySamples = (int)(sampleRate * (self->latency + latency)) + 2048;
if(latencySamples < 4096) latencySamples = 4096;
int readCursor = visAudioCursor - latencySamples;
int samples = 4096;
int samplesRead = 0;
if(latencySamples + samples > visAudioSize) {
samples = (int)(visAudioSize - latencySamples);
}
while(readCursor < 0)
readCursor += visAudioSize;
while(readCursor >= visAudioSize)
readCursor -= visAudioSize;
while(samples > 0) {
int samplesToRead = (int)(visAudioSize - readCursor);
if(samplesToRead > samples) samplesToRead = samples;
cblas_scopy(samplesToRead, visAudio + readCursor, 1, visAudioTemp + samplesRead, 1);
samplesRead += samplesToRead;
readCursor += samplesToRead;
samples -= samplesToRead;
if(readCursor >= visAudioSize) readCursor -= visAudioSize;
}
}
if(outPCM) {
cblas_scopy(4096, visAudioTemp, 1, outPCM, 1);
}
if(outFFT) {
fft_calculate(visAudioTemp, outFFT, 2048);
}
free(visAudioTemp);
}
@end

View file

@ -0,0 +1,136 @@
//
// VisualizationController.swift
// CogAudio Framework
//
// Created by Christopher Snowhill on 6/30/22.
//
import Foundation
@objc(VisualizationController)
class VisualizationController : NSObject {
var serialQueue = DispatchQueue(label: "Visualization Queue")
var sampleRate = 0.0
var latency = 0.0
var visAudio: [Float] = Array(repeating: 0.0, count: 44100 * 45)
var visAudioCursor = 0
var visAudioSize = 0
var visSamplesPosted: UInt64 = 0
private static var sharedVisualizationController: VisualizationController = {
let visualizationController = VisualizationController()
return visualizationController
}()
@objc
class func sharedController() -> VisualizationController {
return sharedVisualizationController
}
@objc
func reset() {
serialQueue.sync {
self.latency = 0;
let amount = self.visAudioSize
for i in 0..<amount {
self.visAudio[i] = 0
}
self.visSamplesPosted = 0;
}
}
@objc
func postLatency(_ latency: Double) {
self.latency = latency
}
@objc
func samplesPosted() -> UInt64 {
return self.visSamplesPosted
}
@objc
func postSampleRate(_ sampleRate: Double) {
serialQueue.sync {
if(self.sampleRate != sampleRate) {
self.sampleRate = sampleRate
visAudioSize = (Int)(sampleRate * 45.0)
visAudio = Array(repeating: 0.0, count: visAudioSize)
visAudioCursor = 0
}
}
}
@objc
func postVisPCM(_ inPCM: UnsafePointer<Float>?, amount: Int) {
serialQueue.sync {
let bufferPointer = UnsafeBufferPointer<Float>(start: inPCM, count: amount)
var j = self.visAudioCursor
let k = self.visAudioSize
if(j + amount <= k) {
let endIndex = j + amount;
self.visAudio.replaceSubrange(j..<endIndex, with: bufferPointer)
j += amount
if(j >= k) { j = 0 }
} else {
let inEndIndex = k - j
let remainder = amount - inEndIndex
self.visAudio.replaceSubrange(j..<k, with: bufferPointer.prefix(inEndIndex))
self.visAudio.replaceSubrange(0..<remainder, with: bufferPointer.suffix(remainder))
j = remainder
}
self.visAudioCursor = j
self.visSamplesPosted += UInt64(amount);
}
}
@objc
func readSampleRate() -> Double {
serialQueue.sync {
return self.sampleRate
}
}
@objc
func copyVisPCM(_ outPCM: UnsafeMutablePointer<Float>?, visFFT: UnsafeMutablePointer<Float>?, latencyOffset: Double) {
if(self.visAudioSize == 0) {
outPCM?.update(repeating: 0.0, count: 4096)
visFFT?.update(repeating: 0.0, count: 2048)
return
}
var outPCMCopy = Array<Float>(repeating: 0.0, count: 4096)
serialQueue.sync {
// Offset latency so the target sample is in the center of the window
let latencySamples = (Int)((self.latency + latencyOffset) * self.sampleRate) + 2048
var samplesToDo = 4096;
if(latencySamples < 0) {
return;
}
if(latencySamples < 4096) {
// Latency can sometimes dip below this threshold
samplesToDo = latencySamples;
}
var j = self.visAudioCursor - latencySamples
let k = self.visAudioSize
if j < 0 { j += k }
if(j + samplesToDo <= k) {
outPCMCopy.replaceSubrange(0..<samplesToDo, with: self.visAudio.suffix(from: j).prefix(samplesToDo))
} else {
let outEndIndex = k - j
let remainder = samplesToDo - outEndIndex
outPCMCopy.replaceSubrange(0..<outEndIndex, with: self.visAudio.suffix(from: j))
outPCMCopy.replaceSubrange(outEndIndex..<samplesToDo, with: self.visAudio.prefix(remainder))
}
}
outPCM?.update(from: outPCMCopy, count: 4096)
if(visFFT != nil) {
serialQueue.sync {
fft_calculate(outPCMCopy, visFFT, 2048)
}
}
}
}

View file

@ -1,18 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="20037" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="22113.1" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="20037"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22113.1"/>
<plugIn identifier="com.apple.WebKit2IBPlugin" version="22113.1"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="AboutWIndowController" customModule="Cog" customModuleProvider="target">
<customObject id="-2" userLabel="File's Owner" customClass="AboutWindowController" customModule="Cog" customModuleProvider="target">
<connections>
<outlet property="appCopyright" destination="ht5-N1-YXd" id="cUK-kB-F2V"/>
<outlet property="appName" destination="Lya-f1-R7S" id="ZVN-sE-MQp"/>
<outlet property="appVersion" destination="7dE-CE-MBv" id="qOe-3F-tMG"/>
<outlet property="creditsView" destination="iz8-0U-jtY" id="uMm-WQ-Fui"/>
<outlet property="imageView" destination="aIO-L9-zMj" id="cIl-Ce-onP"/>
<outlet property="creditsView" destination="LZw-Xy-wYl" id="jyQ-Rg-oqx"/>
<outlet property="vfxView" destination="2nK-dq-1h6" id="ToT-af-ycD"/>
<outlet property="window" destination="F0z-JX-Cv5" id="gIp-Ho-8D9"/>
</connections>
@ -39,41 +39,17 @@
<visualEffectView blendingMode="withinWindow" material="fullScreenUI" state="followsWindowActiveState" translatesAutoresizingMaskIntoConstraints="NO" id="2nK-dq-1h6">
<rect key="frame" x="387" y="84" width="262" height="230"/>
<subviews>
<scrollView appearanceType="aqua" borderType="none" horizontalLineScroll="10" horizontalPageScroll="10" verticalLineScroll="10" verticalPageScroll="10" hasHorizontalScroller="NO" translatesAutoresizingMaskIntoConstraints="NO" id="HLG-H1-ARK">
<wkWebView wantsLayer="YES" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="LZw-Xy-wYl">
<rect key="frame" x="0.0" y="0.0" width="262" height="230"/>
<clipView key="contentView" drawsBackground="NO" id="VsR-Lq-AhX">
<rect key="frame" x="0.0" y="0.0" width="247" height="230"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textView editable="NO" verticallyResizable="YES" findStyle="bar" allowsDocumentBackgroundColorChange="YES" spellingCorrection="YES" smartInsertDelete="YES" id="iz8-0U-jtY">
<rect key="frame" x="0.0" y="0.0" width="247" height="230"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<size key="minSize" width="247" height="230"/>
<size key="maxSize" width="247" height="10000000"/>
<color key="insertionPointColor" name="textColor" catalog="System" colorSpace="catalog"/>
</textView>
</subviews>
</clipView>
<scroller key="horizontalScroller" hidden="YES" verticalHuggingPriority="750" horizontal="YES" id="Jrl-HU-vmJ">
<rect key="frame" x="-100" y="-100" width="225" height="15"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
<scroller key="verticalScroller" verticalHuggingPriority="750" horizontal="NO" id="z2b-Sa-SQ9">
<rect key="frame" x="247" y="0.0" width="15" height="230"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
</scrollView>
<autoresizingMask key="autoresizingMask"/>
<wkWebViewConfiguration key="configuration" applicationNameForUserAgent="Cog">
<audiovisualMediaTypes key="mediaTypesRequiringUserActionForPlayback" none="YES"/>
<wkPreferences key="preferences"/>
</wkWebViewConfiguration>
</wkWebView>
</subviews>
<constraints>
<constraint firstItem="HLG-H1-ARK" firstAttribute="leading" secondItem="2nK-dq-1h6" secondAttribute="leading" id="RIy-GD-jCE"/>
<constraint firstItem="HLG-H1-ARK" firstAttribute="top" secondItem="2nK-dq-1h6" secondAttribute="top" id="Ye7-Xs-CXc"/>
<constraint firstAttribute="trailing" secondItem="HLG-H1-ARK" secondAttribute="trailing" id="tZ8-S3-bky"/>
<constraint firstAttribute="bottom" secondItem="HLG-H1-ARK" secondAttribute="bottom" id="vNr-Wi-0vq"/>
</constraints>
</visualEffectView>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Lya-f1-R7S">
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Lya-f1-R7S">
<rect key="frame" x="385" y="351" width="266" height="31"/>
<textFieldCell key="cell" lineBreakMode="clipping" selectable="YES" title="Cog" usesSingleLineMode="YES" id="hzl-Rl-e01">
<font key="font" textStyle="largeTitle" name=".SFNS-Regular"/>
@ -81,7 +57,7 @@
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="7dE-CE-MBv">
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="7dE-CE-MBv">
<rect key="frame" x="385" y="322" width="266" height="21"/>
<textFieldCell key="cell" lineBreakMode="clipping" selectable="YES" title="Version..." usesSingleLineMode="YES" id="Wjf-By-C1F">
<font key="font" textStyle="title2" name=".SFNS-Regular"/>
@ -89,12 +65,12 @@
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="ht5-N1-YXd">
<textField focusRingType="none" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="ht5-N1-YXd">
<rect key="frame" x="387" y="20" width="264" height="56"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" selectable="YES" title="Copyright..." id="wLU-AJ-J0b">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="textColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>

View file

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="19529" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="20037" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="19529"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="20037"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
@ -853,7 +853,7 @@
</connections>
</slider>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="sTm-FX-lSj">
<rect key="frame" x="172" y="9" width="356" height="16"/>
<rect key="frame" x="0.0" y="9" width="701" height="16"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="center" title="Note: You may use right-click to draw an equalizer shape" id="lwG-Tm-rr1">
<font key="font" usesAppearanceFont="YES"/>

View file

@ -1,33 +1,62 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="15505" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="23504" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="15505"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="23504"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="FeedbackController">
<connections>
<outlet property="fromView" destination="5" id="37"/>
<outlet property="messageView" destination="15" id="38"/>
<outlet property="sendingIndicator" destination="12" id="39"/>
<outlet property="subjectView" destination="10" id="36"/>
<outlet property="emailView" destination="10" id="X4O-Qx-zUq"/>
<outlet property="messageView" destination="15" id="gwe-Bb-ZEz"/>
<outlet property="nameView" destination="5" id="9Uz-Hh-EVf"/>
<outlet property="window" destination="1" id="42"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<window title="Send Feedback" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" id="1" userLabel="FeedbackWindow">
<window title="Send Crash Feedback" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" id="1" userLabel="FeedbackWindow">
<windowStyleMask key="styleMask" titled="YES" closable="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="168" y="357" width="480" height="376"/>
<rect key="screenRect" x="0.0" y="0.0" width="1920" height="1057"/>
<rect key="screenRect" x="0.0" y="0.0" width="1920" height="1055"/>
<value key="minSize" type="size" width="213" height="107"/>
<view key="contentView" id="3">
<rect key="frame" x="0.0" y="0.0" width="480" height="376"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="10">
<textField focusRingType="none" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="8" userLabel="Name:">
<rect key="frame" x="17" y="339" width="58" height="17"/>
<autoresizingMask key="autoresizingMask"/>
<textFieldCell key="cell" sendsActionOnEndEditing="YES" alignment="left" title="Name:" id="18" userLabel="Name:">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="5" userLabel="Name Field">
<rect key="frame" x="80" y="337" width="356" height="22"/>
<autoresizingMask key="autoresizingMask"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" state="on" borderStyle="bezel" drawsBackground="YES" id="21">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<connections>
<outlet property="nextKeyView" destination="10" id="CNG-sG-ab3"/>
</connections>
</textField>
<textField focusRingType="none" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="4">
<rect key="frame" x="17" y="297" width="58" height="17"/>
<autoresizingMask key="autoresizingMask"/>
<textFieldCell key="cell" sendsActionOnEndEditing="YES" title="Email:" id="22">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="10" userLabel="Email Field">
<rect key="frame" x="80" y="295" width="356" height="22"/>
<autoresizingMask key="autoresizingMask"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" state="on" borderStyle="bezel" alignment="left" drawsBackground="YES" id="16">
@ -36,22 +65,13 @@
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<connections>
<outlet property="nextKeyView" destination="15" id="30"/>
<outlet property="nextKeyView" destination="15" id="Xi7-6Y-bG1"/>
</connections>
</textField>
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="8">
<rect key="frame" x="17" y="297" width="58" height="17"/>
<textField focusRingType="none" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="7">
<rect key="frame" x="17" y="262" width="272" height="17"/>
<autoresizingMask key="autoresizingMask"/>
<textFieldCell key="cell" sendsActionOnEndEditing="YES" alignment="left" title="Subject:" id="18">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="7">
<rect key="frame" x="17" y="262" width="66" height="17"/>
<autoresizingMask key="autoresizingMask"/>
<textFieldCell key="cell" sendsActionOnEndEditing="YES" alignment="left" title="Message:" id="19">
<textFieldCell key="cell" sendsActionOnEndEditing="YES" alignment="left" title="Describe what you were doing:" id="19">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
@ -60,16 +80,16 @@
<scrollView fixedFrame="YES" horizontalLineScroll="10" horizontalPageScroll="10" verticalLineScroll="10" verticalPageScroll="10" hasHorizontalScroller="NO" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="11">
<rect key="frame" x="20" y="55" width="440" height="199"/>
<autoresizingMask key="autoresizingMask"/>
<clipView key="contentView" ambiguous="YES" drawsBackground="NO" id="tK9-bv-5OD">
<rect key="frame" x="1" y="1" width="438" height="197"/>
<clipView key="contentView" drawsBackground="NO" id="tK9-bv-5OD">
<rect key="frame" x="1" y="1" width="423" height="197"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textView ambiguous="YES" importsGraphics="NO" verticallyResizable="YES" usesFontPanel="YES" findStyle="panel" continuousSpellChecking="YES" usesRuler="YES" smartInsertDelete="YES" id="15">
<rect key="frame" x="0.0" y="0.0" width="438" height="197"/>
<textView importsGraphics="NO" verticallyResizable="YES" usesFontPanel="YES" findStyle="panel" continuousSpellChecking="YES" usesRuler="YES" smartInsertDelete="YES" id="15">
<rect key="frame" x="0.0" y="0.0" width="423" height="197"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
<size key="minSize" width="438" height="197"/>
<size key="minSize" width="423" height="197"/>
<size key="maxSize" width="863" height="10000000"/>
<color key="insertionPointColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
</textView>
@ -80,7 +100,7 @@
<autoresizingMask key="autoresizingMask"/>
</scroller>
<scroller key="verticalScroller" wantsLayer="YES" verticalHuggingPriority="750" horizontal="NO" id="14">
<rect key="frame" x="423" y="1" width="16" height="197"/>
<rect key="frame" x="424" y="1" width="15" height="197"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
<connections>
@ -111,31 +131,6 @@
<outlet property="nextKeyView" destination="5" id="28"/>
</connections>
</button>
<progressIndicator horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" maxValue="100" bezeled="NO" indeterminate="YES" controlSize="small" style="spinning" translatesAutoresizingMaskIntoConstraints="NO" id="12">
<rect key="frame" x="444" y="340" width="16" height="16"/>
<autoresizingMask key="autoresizingMask"/>
</progressIndicator>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="5">
<rect key="frame" x="80" y="337" width="356" height="22"/>
<autoresizingMask key="autoresizingMask"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" state="on" borderStyle="bezel" drawsBackground="YES" id="21">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<connections>
<outlet property="nextKeyView" destination="10" id="33"/>
</connections>
</textField>
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="4">
<rect key="frame" x="17" y="339" width="71" height="17"/>
<autoresizingMask key="autoresizingMask"/>
<textFieldCell key="cell" sendsActionOnEndEditing="YES" title="Email:" id="22">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
</view>
<connections>

View file

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14313.18" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="22113.1" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14313.18"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22113.1"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
@ -32,10 +32,10 @@
<rect key="frame" x="0.0" y="0.0" width="300" height="400"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<pathControl focusRingType="none" verticalHuggingPriority="750" allowsExpansionToolTips="YES" id="65">
<rect key="frame" x="0.0" y="374" width="300" height="26"/>
<pathControl focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" allowsExpansionToolTips="YES" translatesAutoresizingMaskIntoConstraints="NO" id="65">
<rect key="frame" x="76" y="374" width="224" height="26"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<pathCell key="cell" selectable="YES" editable="YES" focusRingType="none" alignment="left" pathStyle="popUp" id="66">
<pathCell key="cell" selectable="YES" enabled="NO" focusRingType="none" alignment="left" pathStyle="popUp" id="66">
<font key="font" metaFont="system"/>
<color key="backgroundColor" name="windowBackgroundColor" catalog="System" colorSpace="catalog"/>
</pathCell>
@ -47,7 +47,7 @@
</binding>
</connections>
</pathControl>
<box boxType="custom" borderType="line" title="Box" titlePosition="noTitle" id="147">
<box fixedFrame="YES" boxType="custom" borderType="line" title="Box" titlePosition="noTitle" translatesAutoresizingMaskIntoConstraints="NO" id="147">
<rect key="frame" x="0.0" y="373" width="300" height="1"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<view key="contentView" id="Dg2-ay-LZH">
@ -58,7 +58,7 @@
<color key="fillColor" name="textColor" catalog="System" colorSpace="catalog"/>
<font key="titleFont" metaFont="system"/>
</box>
<scrollView borderType="none" autohidesScrollers="YES" horizontalLineScroll="23" horizontalPageScroll="10" verticalLineScroll="23" verticalPageScroll="10" usesPredominantAxisScrolling="NO" id="64">
<scrollView fixedFrame="YES" borderType="none" autohidesScrollers="YES" horizontalLineScroll="23" horizontalPageScroll="10" verticalLineScroll="23" verticalPageScroll="10" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="64">
<rect key="frame" x="0.0" y="0.0" width="300" height="373"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<clipView key="contentView" drawsBackground="NO" copiesOnScroll="NO" id="OYe-Aa-Spw">
@ -67,14 +67,13 @@
<subviews>
<outlineView focusRingType="none" verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="lastColumnOnly" selectionHighlightStyle="sourceList" columnReordering="NO" autosaveColumns="NO" autosaveName="FileTree" rowHeight="18" indentationPerLevel="14" autoresizesOutlineColumn="YES" outlineTableColumn="70" id="69" customClass="FileTreeOutlineView">
<rect key="frame" x="0.0" y="0.0" width="300" height="373"/>
<autoresizingMask key="autoresizingMask" flexibleMaxY="YES"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<size key="intercellSpacing" width="3" height="5"/>
<color key="backgroundColor" name="_sourceListBackgroundColor" catalog="System" colorSpace="catalog"/>
<color key="gridColor" name="gridColor" catalog="System" colorSpace="catalog"/>
<tableColumns>
<tableColumn editable="NO" width="297" minWidth="16" maxWidth="1000" id="70">
<tableColumn editable="NO" width="268" minWidth="16" maxWidth="1000" id="70">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="left">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" white="0.33333299" alpha="1" colorSpace="calibratedWhite"/>
</tableHeaderCell>
@ -115,7 +114,19 @@
<outlet property="nextKeyView" destination="69" id="104"/>
</connections>
</scrollView>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Tqu-Wl-fBM">
<rect key="frame" x="-2" y="370" width="81" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="push" title="Choose" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="vRe-3U-Nxj">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="chooseRootFolder:" target="-2" id="aZ6-tK-wqz"/>
</connections>
</button>
</subviews>
<point key="canvasLocation" x="-310" y="119"/>
</customView>
<customObject id="94" customClass="FileTreeController">
<connections>

View file

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="20037" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="23504" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="20037"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="23504"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
@ -13,18 +13,39 @@
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<window title="Info Inspector" separatorStyle="none" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" hidesOnDeactivate="YES" visibleAtLaunch="NO" frameAutosaveName="InfoInspector" animationBehavior="default" titlebarAppearsTransparent="YES" id="1" customClass="NSPanel">
<window title="Info Inspector" separatorStyle="none" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" hidesOnDeactivate="YES" visibleAtLaunch="NO" animationBehavior="utilityWindow" frameAutosaveName="InfoInspector" titlebarAppearsTransparent="YES" id="1" customClass="NSPanel">
<windowStyleMask key="styleMask" titled="YES" closable="YES" resizable="YES" utility="YES" nonactivatingPanel="YES" HUD="YES"/>
<rect key="contentRect" x="700" y="80" width="300" height="582"/>
<rect key="contentRect" x="700" y="80" width="300" height="604"/>
<rect key="screenRect" x="0.0" y="0.0" width="1920" height="1055"/>
<value key="minSize" type="size" width="240" height="550"/>
<value key="maxSize" type="size" width="400" height="600"/>
<value key="minSize" type="size" width="240" height="594"/>
<value key="maxSize" type="size" width="400" height="622"/>
<view key="contentView" id="2">
<rect key="frame" x="0.0" y="0.0" width="300" height="582"/>
<rect key="frame" x="0.0" y="0.0" width="300" height="604"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="9">
<rect key="frame" x="60" y="526" width="45" height="14"/>
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="vB6-9J-5qg">
<rect key="frame" x="-2" y="592" width="107" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Album Artist:" id="LFJ-QQ-gGr">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="cj0-Tw-xpq" customClass="ToolTipTextField">
<rect key="frame" x="113" y="592" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" selectable="YES" sendsActionOnEndEditing="YES" title="N/A" id="B8w-o8-ZBw">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<connections>
<binding destination="-2" name="value" keyPath="valueToDisplay.albumartist" id="gTS-bf-rHT"/>
</connections>
</textField>
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="9">
<rect key="frame" x="-2" y="570" width="107" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Artist:" id="10">
<font key="font" metaFont="smallSystem"/>
@ -32,100 +53,10 @@
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="11">
<rect key="frame" x="62" y="504" width="43" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Album:" id="12">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="13">
<rect key="frame" x="67" y="460" width="38" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Track:" id="14">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="15">
<rect key="frame" x="60" y="438" width="45" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Length:" id="16">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="17">
<rect key="frame" x="73" y="416" width="32" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Year:" id="18">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="19">
<rect key="frame" x="65" y="394" width="40" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Genre:" id="20">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="21">
<rect key="frame" x="32" y="350" width="73" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Sample Rate:" id="22">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="27">
<rect key="frame" x="48" y="328" width="57" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Channels:" id="28">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="29">
<rect key="frame" x="63" y="306" width="42" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Bitrate:" id="32">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="30">
<rect key="frame" x="15" y="284" width="90" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Bits Per Sample:" id="31">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="23">
<rect key="frame" x="73" y="482" width="32" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Title:" id="24">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="33" customClass="ToolTipTextField">
<rect key="frame" x="113" y="526" width="170" height="14"/>
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="33" customClass="ToolTipTextField">
<rect key="frame" x="113" y="570" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" sendsActionOnEndEditing="YES" title="N/A" id="34">
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" selectable="YES" sendsActionOnEndEditing="YES" title="N/A" id="34">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
@ -134,10 +65,40 @@
<binding destination="-2" name="value" keyPath="valueToDisplay.artist" id="108"/>
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="35" customClass="ToolTipTextField">
<rect key="frame" x="113" y="504" width="170" height="14"/>
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="fDr-Nh-3YI">
<rect key="frame" x="-2" y="548" width="107" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Composer:" id="z22-KP-d7B">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="WVx-cb-45q" customClass="ToolTipTextField">
<rect key="frame" x="113" y="548" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" sendsActionOnEndEditing="YES" title="N/A" id="36">
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" selectable="YES" sendsActionOnEndEditing="YES" title="N/A" id="AUa-jh-Azn">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<connections>
<binding destination="-2" name="value" keyPath="valueToDisplay.composer" id="AKm-26-c5F"/>
</connections>
</textField>
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="11">
<rect key="frame" x="-2" y="526" width="107" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Album:" id="12">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="35" customClass="ToolTipTextField">
<rect key="frame" x="113" y="526" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" selectable="YES" sendsActionOnEndEditing="YES" title="N/A" id="36">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
@ -146,10 +107,19 @@
<binding destination="-2" name="value" keyPath="valueToDisplay.album" id="109"/>
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="37" customClass="ToolTipTextField">
<rect key="frame" x="113" y="482" width="170" height="14"/>
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="23">
<rect key="frame" x="-2" y="504" width="107" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Title:" id="24">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="37" customClass="ToolTipTextField">
<rect key="frame" x="113" y="504" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" sendsActionOnEndEditing="YES" title="N/A" id="38">
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" selectable="YES" sendsActionOnEndEditing="YES" title="N/A" id="38">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
@ -158,10 +128,19 @@
<binding destination="-2" name="value" keyPath="valueToDisplay.title" id="110"/>
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="39" customClass="ToolTipTextField">
<rect key="frame" x="113" y="460" width="170" height="14"/>
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="13">
<rect key="frame" x="-2" y="482" width="107" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Track:" id="14">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="39" customClass="ToolTipTextField">
<rect key="frame" x="113" y="482" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" sendsActionOnEndEditing="YES" title="N/A" id="40">
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" selectable="YES" sendsActionOnEndEditing="YES" title="N/A" id="40">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
@ -170,34 +149,62 @@
<binding destination="-2" name="value" keyPath="valueToDisplay.trackText" id="ZO2-Cd-dfh"/>
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="41" customClass="ToolTipTextField">
<rect key="frame" x="113" y="438" width="170" height="14"/>
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="15">
<rect key="frame" x="-2" y="460" width="107" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Length:" id="16">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="41" customClass="ToolTipTextField">
<rect key="frame" x="113" y="460" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" sendsActionOnEndEditing="YES" title="N/A" id="42">
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" selectable="YES" sendsActionOnEndEditing="YES" title="N/A" id="42">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<connections>
<binding destination="-2" name="toolTip" keyPath="valueToDisplay.lengthInfo" id="26q-iJ-ecn"/>
<binding destination="-2" name="value" keyPath="valueToDisplay.lengthText" id="ji7-tL-8rb"/>
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="43" customClass="ToolTipTextField">
<rect key="frame" x="113" y="416" width="170" height="14"/>
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="17">
<rect key="frame" x="-2" y="438" width="107" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Date:" id="18">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="43" customClass="ToolTipTextField">
<rect key="frame" x="113" y="438" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" sendsActionOnEndEditing="YES" title="N/A" id="44">
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" selectable="YES" sendsActionOnEndEditing="YES" title="N/A" id="44">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<connections>
<binding destination="-2" name="value" keyPath="valueToDisplay.yearText" id="miZ-gp-CqU"/>
<binding destination="-2" name="value" keyPath="valueToDisplay.date" id="EUq-4E-Lz3"/>
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="45" customClass="ToolTipTextField">
<rect key="frame" x="113" y="394" width="170" height="14"/>
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="19">
<rect key="frame" x="-2" y="416" width="107" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Genre:" id="20">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="45" customClass="ToolTipTextField">
<rect key="frame" x="113" y="416" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" sendsActionOnEndEditing="YES" title="N/A" id="46">
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" selectable="YES" sendsActionOnEndEditing="YES" title="N/A" id="46">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
@ -206,22 +213,65 @@
<binding destination="-2" name="value" keyPath="valueToDisplay.genre" id="114"/>
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="49" customClass="ToolTipTextField">
<rect key="frame" x="113" y="350" width="170" height="14"/>
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="84">
<rect key="frame" x="-2" y="394" width="107" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Filename:" id="85">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="86" customClass="ToolTipTextField">
<rect key="frame" x="113" y="394" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" sendsActionOnEndEditing="YES" title="N/A" id="50">
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" selectable="YES" sendsActionOnEndEditing="YES" title="N/A" id="87">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<connections>
<binding destination="-2" name="value" keyPath="valueToDisplay.sampleRate" id="116"/>
<binding destination="-2" name="value" keyPath="valueToDisplay.filename" id="115"/>
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="51" customClass="ToolTipTextField">
<rect key="frame" x="113" y="328" width="170" height="14"/>
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="21">
<rect key="frame" x="-2" y="372" width="107" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Sample Rate:" id="22">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="49" customClass="ToolTipTextField">
<rect key="frame" x="113" y="372" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" sendsActionOnEndEditing="YES" title="N/A" id="52">
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" selectable="YES" sendsActionOnEndEditing="YES" title="N/A" id="50">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<connections>
<binding destination="-2" name="value" keyPath="valueToDisplay.sampleRate" id="jhK-B4-LCu">
<dictionary key="options">
<string key="NSValueTransformerName">NumberHertzToStringTransformer</string>
</dictionary>
</binding>
</connections>
</textField>
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="27">
<rect key="frame" x="-2" y="350" width="107" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Channels:" id="28">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="51" customClass="ToolTipTextField">
<rect key="frame" x="113" y="350" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" selectable="YES" sendsActionOnEndEditing="YES" title="N/A" id="52">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
@ -230,10 +280,19 @@
<binding destination="-2" name="value" keyPath="valueToDisplay.channels" id="117"/>
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="53" customClass="ToolTipTextField">
<rect key="frame" x="113" y="306" width="170" height="14"/>
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="29">
<rect key="frame" x="-2" y="328" width="107" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Bitrate:" id="32">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="53" customClass="ToolTipTextField">
<rect key="frame" x="113" y="328" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" sendsActionOnEndEditing="YES" title="N/A" id="54">
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" selectable="YES" sendsActionOnEndEditing="YES" title="N/A" id="54">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
@ -242,10 +301,19 @@
<binding destination="-2" name="value" keyPath="valueToDisplay.bitrate" id="118"/>
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="55" customClass="ToolTipTextField">
<rect key="frame" x="113" y="284" width="170" height="14"/>
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="30">
<rect key="frame" x="-2" y="306" width="107" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Bits Per Sample:" id="31">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="55" customClass="ToolTipTextField">
<rect key="frame" x="113" y="306" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" sendsActionOnEndEditing="YES" title="N/A" id="56">
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" selectable="YES" sendsActionOnEndEditing="YES" title="N/A" id="56">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
@ -254,8 +322,8 @@
<binding destination="-2" name="value" keyPath="valueToDisplay.bitsPerSample" id="122"/>
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="QPg-Mb-Urn">
<rect key="frame" x="60" y="262" width="45" height="14"/>
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="QPg-Mb-Urn">
<rect key="frame" x="-2" y="284" width="107" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Codec:" id="cbq-TT-CZX">
<font key="font" metaFont="smallSystem"/>
@ -263,10 +331,10 @@
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="ijS-y2-eCZ" customClass="ToolTipTextField">
<rect key="frame" x="113" y="262" width="170" height="14"/>
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="ijS-y2-eCZ" customClass="ToolTipTextField">
<rect key="frame" x="113" y="284" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" sendsActionOnEndEditing="YES" title="N/A" id="Yby-OU-cqP">
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" selectable="YES" sendsActionOnEndEditing="YES" title="N/A" id="Yby-OU-cqP">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
@ -275,8 +343,8 @@
<binding destination="-2" name="value" keyPath="valueToDisplay.codec" id="Tle-Vx-BN5"/>
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="bti-s6-SIU">
<rect key="frame" x="38" y="240" width="67" height="14"/>
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="bti-s6-SIU">
<rect key="frame" x="-2" y="262" width="107" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Encoding:" id="8e7-lp-K5l">
<font key="font" metaFont="smallSystem"/>
@ -284,10 +352,10 @@
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="L4f-rE-CN3" customClass="ToolTipTextField">
<rect key="frame" x="113" y="240" width="170" height="14"/>
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="L4f-rE-CN3" customClass="ToolTipTextField">
<rect key="frame" x="113" y="262" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" sendsActionOnEndEditing="YES" title="N/A" id="v14-AG-B9D">
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" selectable="YES" sendsActionOnEndEditing="YES" title="N/A" id="v14-AG-B9D">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
@ -296,8 +364,8 @@
<binding destination="-2" name="value" keyPath="valueToDisplay.encoding" id="BWi-9r-yOR"/>
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="gn9-9b-eV8">
<rect key="frame" x="15" y="218" width="90" height="14"/>
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="gn9-9b-eV8">
<rect key="frame" x="-2" y="240" width="107" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Cuesheet:" id="Cu5-ia-Z5b">
<font key="font" metaFont="smallSystem"/>
@ -305,10 +373,10 @@
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="WOl-SC-4tu" customClass="ToolTipTextField">
<rect key="frame" x="113" y="218" width="170" height="14"/>
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="WOl-SC-4tu" customClass="ToolTipTextField">
<rect key="frame" x="113" y="240" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" sendsActionOnEndEditing="YES" title="N/A" id="UyE-Mc-e39">
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" selectable="YES" sendsActionOnEndEditing="YES" title="N/A" id="UyE-Mc-e39">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
@ -317,8 +385,8 @@
<binding destination="-2" name="value" keyPath="valueToDisplay.cuesheetPresent" id="nPu-MP-igV"/>
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="arA-Pj-ANg">
<rect key="frame" x="15" y="196" width="90" height="14"/>
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="arA-Pj-ANg">
<rect key="frame" x="-2" y="218" width="107" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="ReplayGain:" id="qBi-M8-kEx">
<font key="font" metaFont="smallSystem"/>
@ -326,10 +394,10 @@
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="2xx-It-i6I" customClass="ToolTipTextField">
<rect key="frame" x="113" y="196" width="170" height="14"/>
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="2xx-It-i6I" customClass="ToolTipTextField">
<rect key="frame" x="113" y="218" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" sendsActionOnEndEditing="YES" title="N/A" id="rAo-AP-lVa">
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" selectable="YES" sendsActionOnEndEditing="YES" title="N/A" id="rAo-AP-lVa">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
@ -339,64 +407,8 @@
<binding destination="-2" name="toolTip" keyPath="valueToDisplay.gainInfo" id="BUp-Hu-Szq"/>
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="84">
<rect key="frame" x="49" y="372" width="56" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Filename:" id="85">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="86" customClass="ToolTipTextField">
<rect key="frame" x="113" y="372" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" sendsActionOnEndEditing="YES" title="N/A" id="87">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<connections>
<binding destination="-2" name="value" keyPath="valueToDisplay.filename" id="115"/>
</connections>
</textField>
<imageView fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="95">
<rect key="frame" x="17" y="17" width="266" height="138"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<imageCell key="cell" enabled="NO" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="96"/>
<connections>
<binding destination="-2" name="value" keyPath="valueToDisplay.albumArt" id="121">
<dictionary key="options">
<bool key="NSAllowsEditingMultipleValuesSelection" value="NO"/>
<bool key="NSConditionallySetsEnabled" value="NO"/>
<string key="NSValueTransformerName">MissingAlbumArtTransformer</string>
</dictionary>
</binding>
</connections>
</imageView>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="vB6-9J-5qg">
<rect key="frame" x="18" y="548" width="87" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Album Artist:" id="LFJ-QQ-gGr">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="cj0-Tw-xpq" customClass="ToolTipTextField">
<rect key="frame" x="113" y="548" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" sendsActionOnEndEditing="YES" title="N/A" id="B8w-o8-ZBw">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<connections>
<binding destination="-2" name="value" keyPath="valueToDisplay.albumartist" id="gTS-bf-rHT"/>
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="FWx-Sc-Ocv">
<rect key="frame" x="15" y="174" width="90" height="14"/>
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="FWx-Sc-Ocv">
<rect key="frame" x="-2" y="196" width="107" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Play Count:" id="fiv-eh-w3c">
<font key="font" metaFont="smallSystem"/>
@ -404,10 +416,10 @@
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="WSs-wC-mWc" customClass="ToolTipTextField">
<rect key="frame" x="113" y="174" width="170" height="14"/>
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="WSs-wC-mWc" customClass="ToolTipTextField">
<rect key="frame" x="113" y="196" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" sendsActionOnEndEditing="YES" title="N/A" id="Ial-XI-91y">
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" selectable="YES" sendsActionOnEndEditing="YES" title="N/A" id="Ial-XI-91y">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
@ -417,6 +429,42 @@
<binding destination="-2" name="toolTip" keyPath="valueToDisplay.playCountInfo" id="ydF-ec-fBX"/>
</connections>
</textField>
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="cd3-Qt-hCm">
<rect key="frame" x="-2" y="174" width="107" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Comment:" id="Ule-N3-dKW">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Ef3-yG-qT1" customClass="ToolTipTextField">
<rect key="frame" x="113" y="174" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" selectable="YES" sendsActionOnEndEditing="YES" title="N/A" id="PPV-dt-9Bp">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<connections>
<binding destination="-2" name="value" keyPath="valueToDisplay.comment" id="Esa-PB-mpv"/>
<binding destination="-2" name="toolTip" keyPath="valueToDisplay.comment" id="XzQ-nd-jMU"/>
</connections>
</textField>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="RWn-fb-0wT">
<rect key="frame" x="0.0" y="20" width="300" height="146"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxX="YES" flexibleMinY="YES" heightSizable="YES"/>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="vwy-pi-T4L"/>
<connections>
<binding destination="-2" name="value" keyPath="valueToDisplay.albumArt" id="bmd-73-heg">
<dictionary key="options">
<bool key="NSAllowsEditingMultipleValuesSelection" value="NO"/>
<bool key="NSConditionallySetsEnabled" value="NO"/>
<string key="NSValueTransformerName">MissingAlbumArtTransformer</string>
</dictionary>
</binding>
</connections>
</imageView>
</subviews>
</view>
<point key="canvasLocation" x="136" y="131"/>

View file

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="22113.1" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22113.1"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="LyricsWindowController">
<connections>
<outlet property="window" destination="QvC-M9-y7g" id="eSJ-Nv-PBE"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<window title="Lyrics" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" visibleAtLaunch="NO" frameAutosaveName="LyricsWindow" animationBehavior="default" id="QvC-M9-y7g">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="196" y="240" width="480" height="270"/>
<rect key="screenRect" x="0.0" y="0.0" width="1920" height="1055"/>
<view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ">
<rect key="frame" x="0.0" y="0.0" width="480" height="270"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<scrollView borderType="none" horizontalLineScroll="10" horizontalPageScroll="10" verticalLineScroll="10" verticalPageScroll="10" hasHorizontalScroller="NO" id="O8B-8Z-Mxc">
<rect key="frame" x="0.0" y="0.0" width="480" height="270"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<clipView key="contentView" drawsBackground="NO" id="O6S-QV-ThM">
<rect key="frame" x="0.0" y="0.0" width="480" height="270"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textView wantsLayer="YES" editable="NO" importsGraphics="NO" richText="NO" verticallyResizable="YES" findStyle="bar" incrementalSearchingEnabled="YES" id="DKA-ld-0Sh">
<rect key="frame" x="0.0" y="0.0" width="480" height="270"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
<size key="minSize" width="480" height="270"/>
<size key="maxSize" width="480" height="10000000"/>
<color key="insertionPointColor" name="textColor" catalog="System" colorSpace="catalog"/>
<connections>
<binding destination="-2" name="value" keyPath="valueToDisplay.unsyncedlyrics" id="tSj-CA-G4Q">
<dictionary key="options">
<bool key="NSAllowsEditingMultipleValuesSelection" value="NO"/>
<bool key="NSConditionallySetsEditable" value="NO"/>
</dictionary>
</binding>
</connections>
</textView>
</subviews>
</clipView>
<scroller key="horizontalScroller" hidden="YES" verticalHuggingPriority="750" horizontal="YES" id="f77-wI-xOz">
<rect key="frame" x="-100" y="-100" width="225" height="15"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
<scroller key="verticalScroller" verticalHuggingPriority="750" horizontal="NO" id="XfW-du-B6L">
<rect key="frame" x="464" y="0.0" width="16" height="270"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
</scrollView>
</subviews>
</view>
<point key="canvasLocation" x="126" y="104"/>
</window>
<userDefaultsController representsSharedInstance="YES" id="t5R-DO-d90"/>
</objects>
</document>

Some files were not shown because too many files have changed in this diff Show more