INTERACT FORUM

Please login or register.

Login with username, password and session length
Advanced search  
Pages: [1]   Go Down

Author Topic: ASIO Live-In Sample Rate selection using command line (Windows)  (Read 314 times)

kotani

  • Recent member
  • *
  • Posts: 16
ASIO Live-In Sample Rate selection using command line (Windows)
« on: February 08, 2024, 09:53:50 pm »

Hello. I'm trying to create an intermediate script or some sort that would get the source SPDIF input sample rate, and automatically start the ASIO Live-In with the appropriate sample rate.

1.) Does anyone know if the live://asio can take any parameters to set the sample rate?

2.) Another method would be to know where the location of the configuration file is, that the latest live-in sample rate is stored, so that I can modify it directly before restarting the live-in.

If anyone knows, please help! Thank you in advance! :)
Logged

mattkhan

  • MC Beta Team
  • Citizen of the Universe
  • *****
  • Posts: 3963
Re: ASIO Live-In Sample Rate selection using command line (Windows)
« Reply #1 on: February 09, 2024, 02:49:39 am »

If you open live in and look at playing now then you will see the parameters available and how they are used, save that in the library and then play it via a script would be an easy way to do this
Logged

kotani

  • Recent member
  • *
  • Posts: 16

Thank you mattkhan! I was using a really old version of JRiver before the parameters for the ASIO Live-In was added.

I just wanted to give back to the community by sharing how I was able to accomplish automatic sample rate changes for Digital ASIO Live-In.

Here is some context:

I wanted to connect a iPhone, and use Apple Music to listen to HD music through JRiver. I have a active XO configured though JRiver, which is why I wanted to try to play music through it.

1.) To get digital out from the iPhone, I'm using a Apple Camera USB adapter connected to a SMSL PO100 PRO.

2.) I connect the digital out to a TASCAM US-366 unit for the ASIO Digital ASIO input. It supports sample rates from 44.1khz to 192khz.

3.) I found that the TASCAM software showed the current digital input sample rate in a status window.

4.) I used Cheat Engine for windows to find which memory address in the TASCAM software contained the current sample rate info and also the status text that states "no valid input" in the case there is no connection. (The reason I needed the "no valid input" value is because it remembers the last connected sample rate always, even without a signal.)

5.) I found out that the memory address for the above two values changed every time the computer restarts (or when the TASCAM program is forced to close, and is reopened).

6.) I again used Cheat Engine to find the pointers for the 2 values above so that I can find the correct memory address every time, even after rebooting.

7.) I created a simple command line program in C# (x64) that does the following:

- First, check whether "no valid input" is not currently valid.

- Next, checks current sample rate.

- Runs command line trigger to trigger the ASIO Live-in in JRiver.

Note: The command line to start ASIO Live-In in JRiver:
"mc32.exe /Play "live://asio?deviceindex=4&deviceid=%7BE19D4C7A-2A03-4484-B5A2-8ED346CC0969%7D&samplerate={SAMPLERATE HERE}&channels=2&channelshift=2\"
Note: your deviceindex and deviceid will most likely be different for your hardware. Also, the channel shift being "2" is the right setting for the TASCAM US-366.

-Keeps checking every second for changes in the sample rate. If a new sample rate is found, trigger a new command with the correct sample rate.

- If at any time, "no valid input" becomes TRUE, it will not check for any sample rate, and wait for the input to become valid again.

I run this program along side JRiver, and when I connect the digital connection from the iPhone, the ASIO Live-In automatically switches as needed.


A few notes:
-Depending on the version of the TASCAM software, the pointers and memory locations would probably be different. You will need to find the pointers yourself, if that is your case.
In my case, the pointer address for the two values were located at the following:
**For the Sample Rate (4 byte):
Base Address: "CPL_US322_US366.EXE+003E7A30"
offset: E84

**For the "not valid signal" status text (string):
Base Address: "CPL_US322_US366.EXE+003E7F90"
offset: BEC

FINAL NOTICE: I cannot take any responsibility for any harm this method and code may cause. Please use it as only as reference and at your own risk.

Finally, here is the source code:

Code: [Select]
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;

class MemoryMonitor
{
    [DllImport("kernel32.dll")]
    public static extern IntPtr OpenProcess(int dwDesiredAccess, bool bInheritHandle, int dwProcessId);

    [DllImport("kernel32.dll")]
    public static extern bool ReadProcessMemory(int hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, int dwSize, ref int lpNumberOfBytesRead);

    [DllImport("kernel32.dll")]
    public static extern bool CloseHandle(IntPtr hObject);

    const int PROCESS_VM_READ = 0x0010;
    private static bool monitorCurrentValue = true;

    static void Main()
    {
        int processId = -1;
        IntPtr processHandle = IntPtr.Zero;
        int lastValue = 0;

        while (true)
        {
            if (processId == -1)
            {
                processId = GetProcessIdByName("CPL_US322_US366");
                if (processId != -1)
                {
                    processHandle = OpenProcess(PROCESS_VM_READ, false, processId);
                    Console.WriteLine("Found process and obtained handle. Monitoring started.");
                }
                else
                {
                    Console.WriteLine("Process not found. Retrying in 5 seconds...");
                    Thread.Sleep(5000);
                    continue;
                }
            }

            IntPtr moduleBase = GetModuleBase(processId, "CPL_US322_US366.EXE");
            IntPtr addressForCurrentValue = ResolveAddress(processHandle, moduleBase, 0x3E7A30, 0xE84);
            IntPtr addressForSignalStatus = ResolveAddress(processHandle, moduleBase, 0x3E7F90, 0xBEC);

            string signalStatus = ReadString(processHandle, addressForSignalStatus, 16);
            if (signalStatus != "no valid signal")
            {
                if (monitorCurrentValue)
                {
                    int currentValue = ReadMemory(processHandle, addressForCurrentValue);
                    if (currentValue != lastValue)
                    {
                        Console.WriteLine($"Value changed from {lastValue} to {currentValue}");
                        lastValue = currentValue;
                        TriggerAction(currentValue);
                    }
                }
                monitorCurrentValue = true; // Enable monitoring
            }
            else
            {
                monitorCurrentValue = false; // Disable monitoring
                lastValue = 0;
                Console.WriteLine("No valid signal detected. Monitoring paused.");
            }
            Thread.Sleep(1000);
        }
    }

    static int GetProcessIdByName(string processName)
    {
        Process[] processes = Process.GetProcessesByName(processName);
        if (processes.Length > 0)
        {
            return processes[0].Id;
        }
        return -1;
    }

    static IntPtr GetModuleBase(int processId, string moduleName)
    {
        Process proc = Process.GetProcessById(processId);
        foreach (ProcessModule module in proc.Modules)
        {
            if (module.ModuleName == moduleName)
            {
                return module.BaseAddress;
            }
        }
        return IntPtr.Zero;
    }

    static IntPtr ResolveAddress(IntPtr processHandle, IntPtr baseAddress, int firstOffset, int secondOffset)
    {
        IntPtr intermediateAddress;
        byte[] buffer = new byte[IntPtr.Size]; // Use IntPtr.Size to handle both 32 and 64-bit architectures
        int bytesRead = 0;

        IntPtr firstLevelAddress = IntPtr.Add(baseAddress, firstOffset);
        ReadProcessMemory((int)processHandle, firstLevelAddress, buffer, buffer.Length, ref bytesRead);
        intermediateAddress = (IntPtr)BitConverter.ToInt32(buffer, 0); // Assumes we are on a 32-bit system

        IntPtr finalAddress = IntPtr.Add(intermediateAddress, secondOffset);
        return finalAddress;
    }

    static int ReadMemory(IntPtr processHandle, IntPtr address)
    {
        int bytesRead = 0;
        byte[] buffer = new byte[4]; // Assuming the data is an integer
        if (ReadProcessMemory((int)processHandle, address, buffer, buffer.Length, ref bytesRead))
        {
            return BitConverter.ToInt32(buffer, 0);
        }
        return -1; // Indicate an error or that the process may have closed
    }

    static string ReadString(IntPtr processHandle, IntPtr address, int length)
    {
        int bytesRead = 0;
        byte[] buffer = new byte[length];
        if (ReadProcessMemory((int)processHandle, address, buffer, length, ref bytesRead))
        {
            return Encoding.ASCII.GetString(buffer).TrimEnd('\0');
        }
        return null; // Indicate an error or that the process may have closed
    }

    static void TriggerAction(int currentValue)
    {
        // List of desired sample rates
        int[] validSampleRates = { 44100, 48000, 88200, 96000, 176000, 192000 };

        // Check if currentValue is in the list of validSampleRates
        if (Array.IndexOf(validSampleRates, currentValue) != -1)
        {
            // Format the command to include the current value in the samplerate parameter
            string command = "mc32.exe";
            string args = $"/Play \"live://asio?deviceindex=4&deviceid=%7BE19D4C7A-2A03-4484-B5A2-8ED346CC0969%7D&samplerate={currentValue}&channels=2&channelshift=2\"";

            // Start the external process
            ProcessStartInfo startInfo = new ProcessStartInfo()
            {
                FileName = command,
                Arguments = args,
                UseShellExecute = false,
                RedirectStandardOutput = true,
                CreateNoWindow = true
            };

            try
            {
                using (Process process = Process.Start(startInfo))
                {
                    Console.WriteLine($"Executed command: {command} {args}");
                    // Optional: Read the output
                    string result = process.StandardOutput.ReadToEnd();
                    process.WaitForExit();
                    // Output the result if needed
                    Console.WriteLine(result);
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine("Failed to execute command. Error: " + ex.Message);
            }
        }
        else
        {
            Console.WriteLine($"Current value {currentValue} is not a valid sample rate. Command not executed.");
        }
    }
}

I hope this may help someone who may be trying to accomplish the same thing. I hope in the future, JRiver will implement automatic sample rate detection for ASIO Live-In
Logged
Pages: [1]   Go Up